← All fixes

Fix it

Storing a JWT in localStorage — the XSS risk and how to fix it

High severityCWE-522 (Insufficiently Protected Credentials)
We scanned nearly 2,000 AI-built apps and 1 in 8 shipped a high-severity flaw. Is this one in yours?Scan free →

localStorage is readable by any JavaScript running on your origin, including injected scripts. If you store a session or JWT there, one cross-site scripting bug anywhere on the site is enough for an attacker to read the token and impersonate the user. The safer pattern is to keep the token in an httpOnly cookie that scripts cannot read, so an XSS bug cannot steal the session outright.

Why it's a problem

Tokens in localStorage have no protection against script access, and web apps pull in a lot of third-party script. A compromised dependency, a reflected XSS, or a malicious browser extension can read the token and send it anywhere. Because the token grants a full authenticated session, this turns a contained scripting bug into a complete account takeover.

The pattern

// after login
localStorage.setItem("token", res.token);

// on each request
fetch("/api/me", {
  headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
});

The fix

// server sets the token in an httpOnly cookie the browser sends automatically
res.cookie("session", token, {
  httpOnly: true,      // not readable by JS
  secure: true,        // HTTPS only
  sameSite: "lax",     // limits cross-site sending
  maxAge: 60 * 60 * 1000,
});

// client just makes the request; no token handling in JS
fetch("/api/me", { credentials: "include" });

Why AI tools write this

localStorage is the path of least resistance in a single-page app. It is trivial to write, works the same in every framework, and appears in most quick-start auth tutorials. httpOnly cookies require the server to set them and a little CSRF care, so the assistant defaults to the version that gets a working login fastest, which is the one that leaves the token exposed.

The quick fix

  • Have the server set the token in an httpOnly, secure, sameSite cookie instead of returning it to JS.
  • Send requests with credentials: 'include' and stop attaching the token in JS.
  • Add CSRF protection once you move to cookies (sameSite plus a token for state-changing requests).
  • If you must keep a token in JS, keep it in memory only, never in localStorage or sessionStorage.

Want to know if this pattern is already in a repo you shipped? Scan a public repo free, no account needed.

Scan a repo →

Related: Missing CSRF protection

JWT in localStorage — Why It's Risky and How to Store Tokens Safely: Prbl