Safeguard
Application Security•

ASVS 5.0 V14.3.3: Keep Sensitive Data Out of Browser Storage

ASVS 5.0 V14.3.3 (Level 2): localStorage, sessionStorage, IndexedDB and cookies may hold session tokens but not other sensitive data. How to comply.

Safeguard Security Team
3 min read

ASVS 5.0 V14.3.3 is a Level 2 requirement. Browser storage, including localStorage, sessionStorage, IndexedDB and cookies, must not contain sensitive data. The one exception is session tokens.

Why it matters

Anything in browser storage is readable by any script on the page. One cross site scripting bug, one compromised third party script, or one malicious extension can copy it all. Storage also persists on disk after the tab closes, and on shared machines the next user can read it.

Developers reach for storage because it is easy: a cached profile, a draft with personal details, a feature flag that happens to contain an email address.

How to implement it

Keep state on the server and fetch it when needed. If something must live in the browser, store only a non sensitive identifier.

// Bad
localStorage.setItem("profile", JSON.stringify({ email, nationalId, address }));

// Better: store nothing sensitive, fetch on demand
const profile = await fetch("/api/me", { credentials: "include" }).then(r => r.json());

For session tokens, prefer a cookie with strong flags so scripts cannot read it:

Set-Cookie: session=...; Secure; HttpOnly; SameSite=Lax; Path=/

Token storage in localStorage is common in single page apps but exposes the token to script. If you must keep a token in memory, keep it in a JavaScript variable and refresh it through an HttpOnly cookie.

For offline apps that truly need local data, encrypt it with a key that is not stored beside it, minimize the contents, and clear it at logout (see V14.3.1).

Add a unit test around your storage wrapper that rejects keys and values that look sensitive:

const forbidden = /(password|ssn|card|secret|nationalid)/i;
export function safeSet(key, value) {
  if (forbidden.test(key) || forbidden.test(String(value))) {
    throw new Error("Refusing to store sensitive data in browser storage");
  }
  localStorage.setItem(key, value);
}

This will not catch everything, but it stops obvious mistakes and makes the rule visible in code review.

How to test it

  1. Use the application normally for a full journey: sign up, sign in, edit profile, make a payment.
  2. Open developer tools, Application tab. Read every key in Local Storage, Session Storage, IndexedDB and Cookies.
  3. For each value, ask whether a stranger could use it. Only a session token is allowed.
  4. Decode anything that looks like Base64 or a JWT, because encoded values are not protected.
  5. Check what survives logout and a browser restart.

You can automate step 2 with a Playwright test that dumps storage after login and fails on patterns such as email addresses or long digit strings.

const data = await page.evaluate(() => JSON.stringify({ ...localStorage, ...sessionStorage }));
expect(data).not.toMatch(/[\w.+-]+@[\w-]+\.[\w.]+/);

Common mistakes

  • Caching the whole user object "for performance".
  • Putting personal data into cookies, which are also sent on every request.
  • Storing refresh tokens in localStorage while claiming only session tokens are kept.
  • Forgetting analytics and SDKs that write their own identifiers.
  • Leaving data in IndexedDB after logout.

Quick check

Why are session tokens the exception?

The browser needs something to present to prove the session. Reduce the risk with HttpOnly, Secure and short lifetimes.

Is sessionStorage safe because it clears when the tab closes?

Not for sensitive data. Scripts on the page can still read it while the tab is open, and the data is visible to developer tools.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.