How One Line of JavaScript Steals Tokens from localStorage
A Practical XSS Demonstration with React and Express
Modern web applications love localStorage. It's simple, persistent, and easy to use. Many Single Page Applications store JWTs there without a second thought.
But here's the uncomfortable truth:
If your app ever suffers an XSS vulnerability, storing tokens in localStorage turns a bug into a full account takeover.
In this article, I'll show you a small React + Express proof of concept that demonstrates — clearly and practically — why HTTP-only cookies are fundamentally safer than localStorage for authentication tokens.
The Question That Matters
Security discussions often get abstract. So instead of asking "Which is best practice?", let's ask:
What can an attacker actually do?
Assume the worst:
- An attacker can execute JavaScript in your app (XSS)
- Their goal is to steal authentication tokens
Now let's test both approaches.
Case 1: Token Stored in localStorage
The application logs in successfully and stores the JWT in localStorage.
When the attacker injects JavaScript, they run:
localStorage.getItem("token")The result?
XSS stole token: JWT_SECRET_TOKENThat's it. The token is stolen, sent elsewhere, and reused.
XSS + localStorage = instant account takeover.
Case 2: Token Stored in HTTP-Only Cookie
Now we repeat the same attack, but the token is stored in a cookie with the HttpOnly flag.
The attacker tries:
document.cookieThe result?
Nothing.
The browser blocks access completely. The token is never exposed to JavaScript.
This protection is not a framework feature. It's enforced by the browser itself, as defined in RFC 6265.
"But What About CSRF?"
Yes, cookies introduce CSRF risks. But here's the key difference:
- XSS allows full account takeover
- CSRF is limited and well-mitigated
With SameSite, CSRF tokens, and origin checks, CSRF becomes manageable.
Token theft via XSS does not.
The Takeaway
HTTP-only cookies don't prevent XSS. They prevent XSS from becoming catastrophic.
If your app handles real users, real money, or real data, storing auth tokens in localStorage is a risk you don't need to take.