Sensitive Data in localStorage
Sticky note with passwords on your monitor — that's what storing secrets in localStorage looks like. Anyone who walks by can read them.
What is this?
You have written your passwords on a sticky note and stuck it to your monitor. Anyone who walks by your desk — coworkers, cleaning staff, visitors — can read them. That is what localStorage does with sensitive data. It is a simple key-value store in the browser with zero encryption, zero access control, and zero expiration. Any JavaScript running on your page (including third-party scripts, browser extensions, and injected code from XSS attacks) can read everything in localStorage. It persists forever until explicitly cleared.
Why it matters
- For your visitors: If you store authentication tokens, personal data, or payment information in localStorage, a single XSS vulnerability exposes all of it. Unlike cookies (which can be marked
HttpOnlyto prevent JavaScript access), localStorage is always readable by any script. An attacker who finds one XSS hole gets all your visitors' stored secrets. - For your business: Storing sensitive data in localStorage violates security best practices and likely violates compliance requirements (GDPR, PCI-DSS, HIPAA). A breach caused by improperly stored data has both legal and reputational consequences. "We stored passwords in localStorage" is a headline you do not want.
- The standard: Never store authentication tokens, passwords, personal data, or payment information in localStorage or sessionStorage. Use
HttpOnlycookies for auth tokens (JavaScript cannot access them). Use server-side sessions for sensitive state. localStorage is fine for non-sensitive preferences like theme choice or language setting.
// Server sets the token as an HttpOnly cookie // JavaScript CANNOT read this — safe from XSS response.headers.set( "Set-Cookie", "session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/", );
// Any script on the page can read this
localStorage.setItem("authToken", "eyJhbGciOiJIUzI1NiJ9...");
localStorage.setItem("userEmail", "alice@example.com");
localStorage.setItem("creditCard", "4111111111111111");How to fix it
React / Next.js
Use HttpOnly cookies for authentication and server-side sessions for sensitive data.
// src/app/api/login/route.ts — set auth token as HttpOnly cookie
export async function POST(request: Request) {
const { email, password } = await request.json();
const token = await authenticate(email, password);
const response = Response.json({ success: true });
// HttpOnly: JavaScript cannot access this cookie
// Secure: only sent over HTTPS
// SameSite=Strict: prevents CSRF
response.headers.set(
"Set-Cookie",
`session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`,
);
return response;
}For data you need on the client (like user preferences), only store non-sensitive values:
// SAFE: non-sensitive preferences in localStorage
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "en");
localStorage.setItem("sidebar-collapsed", "true");
// UNSAFE: never store these in localStorage
localStorage.setItem("token", jwt); // use HttpOnly cookie
localStorage.setItem("password", password); // never store passwords client-side
localStorage.setItem("ssn", "123-45-6789"); // never store PII client-side
localStorage.setItem("apiKey", "sk_live_..."); // keep on serverIf you are using a library that stores tokens in localStorage (some auth libraries do this by default), configure it to use cookies instead:
// Example: configuring an auth library to use cookies
const authClient = createAuth({
storage: "cookie", // NOT "localStorage"
cookieOptions: {
httpOnly: true,
secure: true,
sameSite: "strict",
},
});Plain HTML
Set cookies from the server with proper security flags.
<!-- Server response header (set by your backend) -->
<!--
Set-Cookie: session=TOKEN; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400
-->
<script>
// GOOD: only store non-sensitive preferences
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "en");
// BAD: never do this
// localStorage.setItem("token", "...");
// localStorage.setItem("userSSN", "...");
// localStorage.setItem("password", "...");
// For auth state, read from a secure API endpoint
fetch("/api/me", { credentials: "include" })
.then((res) => res.json())
.then((user) => {
// The cookie is sent automatically — no token handling needed
document.getElementById("username").textContent = user.name;
});
</script>localStorage is a public bulletin board, not a safe. The cat does not leave its secrets lying around, and neither should your app.
How the cat scores this
The scanner analyzes client-side JavaScript for localStorage.setItem() and sessionStorage.setItem() calls and examines the key names being used. Keys containing words like "token," "auth," "password," "secret," "key," "session," "credit," or "ssn" are flagged as potential sensitive data storage. The scanner also checks for JWT patterns (base64-encoded strings with dots) being stored. Non-sensitive keys like "theme" or "language" are not flagged.
Further reading
- OWASP: HTML5 Security — localStorage security guidance
- MDN: Set-Cookie HttpOnly — preventing JavaScript cookie access
- Auth0: Token Storage — best practices for storing auth tokens
document.write() Usage
Rewriting the entire page while someone is reading it — that's what document.write() does. There are much better ways to update the DOM.
Insecure API Calls (HTTP)
Sending a postcard instead of a sealed letter — that's what HTTP API calls do. Anyone between you and the server can read everything.