
Auditing Browser Storage for Cross-Account State: A Practical Privacy Walkthrough
When people hear “privacy bug,” they often picture something dramatic: memory corruption, a broken sandbox, or a protocol edge case. In practice, normal browser storage can cause the problem on its own.
IndexedDB, localStorage, the Cache API, and service worker state are useful because they preserve app state across reloads. They are also why privacy boundaries get fuzzy. If you store identity, workspace context, or cached responses in the wrong place, the browser will keep them longer than users expect.
Why a normal storage API can become a privacy bug
Browser storage is not dangerous because it is obscure. It is dangerous because teams treat it as “just client-side.”
That assumption breaks down when storage can:
- reveal that a user visited before
- correlate activity across sessions
- keep account-specific state after logout
- leak tenant or workspace identifiers
- survive a reset users believed was complete
The lesson from IndexedDB-related browser privacy discussions is not “IndexedDB is broken.” The lesson is that ordinary persistence APIs can cross privacy lines when the product model and the browser model do not match.
What IndexedDB and related browser storage actually preserve
I usually map browser persistence into four buckets:
| Storage layer | Typical use | Risk if mishandled |
|---|---|---|
localStorage | tiny app flags, preferences, tokens | easy cross-session tracking |
| IndexedDB | offline data, cached records, app metadata | long-lived identity and workspace leakage |
| Cache API | response caching for offline apps | stale or cross-account content reuse |
| Service worker state | offline behavior, sync, background logic | hidden persistence beyond visible UI |
The mistake is not storing data. The mistake is assuming the browser will clear it when your app logic changes. Logout is not the same thing as storage eviction.
The privacy boundary questions to ask first
Before you test anything, ask what boundary you actually need.
Logout vs account switch vs site-data clear
These are not the same event:
- Logout means the session ends in the app.
- Account switch means the same browser profile moves to another identity.
- Site-data clear means the browser removes stored data for that origin.
A lot of privacy bugs show up because only one of these is handled correctly.
Private browsing vs normal browsing
Private browsing is another boundary, and it has its own rules. If your app behaves differently in private mode, that may be intentional. If it preserves identifiers in a way that defeats the user's expectation of isolation, that is a problem worth looking at.
A safe test workflow for cross-account state
You do not need a lab setup. A single browser profile and two test accounts is enough.
Reproduce with two accounts and one browser profile
Start with account A in a normal profile:
- Log in.
- Use the app long enough to create real state.
- Inspect what the app wrote to storage.
- Log out.
- Log in as account B in the same browser profile.
Now compare what survives.
Inspect IndexedDB, localStorage, Cache API, and service worker state
Open devtools and check:
- Application tab
- IndexedDB databases
- localStorage keys
- Cache storage
- service worker registration and stored data
You are looking for things like:
- user IDs
- workspace IDs
- cached profile or entitlement data
- feature flags tied to one account
- API responses that should not outlive logout
If you need a quick signal, search storage for account names, workspace slugs, or stable IDs that should not be visible after logout.
Compare what survives logout and site-data clearing
Test three actions separately:
- logout
- account switch
- clear site data
If sensitive state survives logout, that is already worth reporting. If it survives site-data clearing, the issue is more serious because the browser boundary itself is not behaving as expected.
Why this matters for SaaS dashboards and multi-tenant apps
This is where the bug becomes real.
Cached identifiers and workspace context
SaaS apps often store:
- selected workspace IDs
- last active project
- permission hints
- routing state
- tenant-specific API cache entries
That is fine until the browser reuses it across identities. Then the UI can leak that another account existed, or worse, it can point the next user into the wrong tenant context.
Feature flags, offline data, and restored sessions
Feature flags and offline caches are especially risky. I have seen apps that restore a “helpful” last session from IndexedDB even after logout. The user thinks they are starting fresh. The browser thinks it is being helpful. The result is privacy leakage.
What a real privacy failure looks like
Correlation across sessions
A bad pattern is when a fresh login still reveals prior browsing history, workspace selection, or device-specific identifiers. That does not require direct content exposure. Correlation itself can be the privacy break.
Unexpected persistence after clearing expected state
If the app says “you are logged out” but storage still contains account-specific records, the logout flow is incomplete. The browser did what it was told. The app did not define a clear deletion policy.
Cross-account or cross-tenant leakage
The worst case is when account B can see cached state from account A. Even if no secret document is exposed, the presence of prior tenant data can violate isolation assumptions.
Defensive patterns that actually help
Minimize long-lived identifiers
Do not store stable user identifiers unless you really need them. Prefer short-lived, scoped values that expire with the session.
Scope client-side state by account or workspace
If storage is unavoidable, namespace it by account or tenant and make the scope explicit. A single flat cache is how cross-account confusion starts.
Clear sensitive storage on logout and switch
Logout should remove:
- app tokens
- user-specific IndexedDB entries
- cached responses tied to identity
- selected workspace state
- service worker data if it is identity-bound
Test private mode and recovery behavior
Private browsing is not a bonus test. It is part of the privacy model. Also test browser restart, crash recovery, and account switching, because persistence bugs often show up there first.
Conclusion
The takeaway is simple: browser storage is part of your security boundary.
IndexedDB is not exotic, and that is exactly why it matters. If you treat normal persistence APIs as harmless convenience layers, you can end up with cross-account state, identity correlation, and privacy bugs that are easy to miss in code review.
The fix is not to avoid browser storage entirely. The fix is to define what should survive, prove that it survives only that long, and clear the rest with the same care you would apply to server-side session data.


