usability.cat
Issue Wiki

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 HttpOnly to 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 HttpOnly cookies 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.
HttpOnly cookie for auth
// 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=/",
);
Token in localStorage
// 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 server

If 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>
High impactsecurity~2 paws

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

On this page