Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Auditing Browser Storage for Cross-Account State: A Practical Privacy Walkthrough

Auditing Browser Storage for Cross-Account State: A Practical Privacy Walkthrough

pr0h0
browser-securityprivacyindexeddbweb-apis
AI Usage (89%)

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 layerTypical useRisk if mishandled
localStoragetiny app flags, preferences, tokenseasy cross-session tracking
IndexedDBoffline data, cached records, app metadatalong-lived identity and workspace leakage
Cache APIresponse caching for offline appsstale or cross-account content reuse
Service worker stateoffline behavior, sync, background logichidden 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:

  1. Logout means the session ends in the app.
  2. Account switch means the same browser profile moves to another identity.
  3. 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:

  1. Log in.
  2. Use the app long enough to create real state.
  3. Inspect what the app wrote to storage.
  4. Log out.
  5. 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.

Share this post

More posts

Comments