Where You Store a Session Token Determines Which Attack Works

Key takeaway: There is no storage location immune to everything. Choose based on which attack you can actually defend against elsewhere, then implement the corresponding mitigation properly.
The Two Real Options
localStorage is readable by any JavaScript executing on the origin. If an attacker achieves cross-site scripting — through a compromised dependency, an advertising frame, or a reflected parameter — the token is exfiltrated in a single line of code. No amount of care elsewhere in the application changes that outcome.
HttpOnly cookies are invisible to JavaScript entirely. Cross-site scripting can still act as the user within the current page, but it cannot steal a durable credential to replay later from somewhere else. The trade is that browsers attach cookies to requests automatically, which is the mechanism that enables cross-site request forgery.
| Attack | localStorage | HttpOnly cookie |
|---|---|---|
| XSS token theft | Fully exposed | Protected |
| CSRF | Not applicable | Requires mitigation |
| Token leaking into error logs | Common | Rare |
| Cross-origin API calls | Simple | Needs CORS configuration |
Why Cookies Usually Win Now
The forgery problem has a well-supported browser solution. Setting SameSite=Lax prevents the cookie being sent on cross-site POST requests, which eliminates the classic attack without any application code at all. It is now the default behaviour in current browsers, so the historical argument against cookies has largely evaporated.
Cross-site scripting has no equivalent single control. It is mitigated through Content Security Policy, disciplined templating, dependency hygiene and sustained vigilance — a permanent programme rather than one setting. Choosing the storage mechanism that survives an XSS is choosing to be resilient against the failure you are less likely to prevent.
The correct cookie configuration is short and every attribute matters:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
HttpOnly blocks script access. Secure prevents transmission over plain HTTP where a network observer could capture it. SameSite=Lax handles forgery. Omitting any single one of them removes a specific, named protection.
The Part Most Implementations Skip
Storage is only half of session security. Lifecycle is the other half, and it is where security reviews most often find gaps.
Rotate the session identifier whenever privilege changes, and especially at login. Failing to rotate permits session fixation, where an attacker plants a known identifier in the victim’s browser before authentication and then inherits the authenticated session afterwards.
Invalidate on the server, not merely in the browser. A logout that deletes the cookie while leaving the session record valid in the store accomplishes nothing against an attacker who already copied the token — they can continue using it indefinitely.
Keep access tokens short-lived and refresh them from a rotating refresh token. A fifteen-minute access token sharply limits the value of any single stolen credential, and rotation means a stolen refresh token becomes detectable when both the attacker and the legitimate user attempt to use it.
Finally, never place a token in a URL. URLs are recorded in server access logs, browser history, analytics payloads and the Referer header sent to third-party domains — four separate places where a credential should never appear.
The Bottom Line
Use HttpOnly, Secure, SameSite cookies for browser sessions. Reserve localStorage tokens for cases where a cross-origin architecture genuinely requires them, and recognise that the choice makes any cross-site scripting flaw immediately catastrophic rather than merely serious. Then get the lifecycle right, because correct storage with a session that never expires is only half a solution.



