localStorage is synchronously readable by every JavaScript file executing on your domain — your own code, third-party analytics, CDN-hosted widgets, and any XSS payload. Auth tokens stored there are exposed to the entire JavaScript surface area of your application. CWE-922 (Insecure Storage of Sensitive Information) and CWE-312 (Cleartext Storage of Sensitive Information) both apply. OWASP A02 lists insecure credential storage as a cryptographic failure. NIST 800-63B §7.1 requires that session secrets be protected from disclosure; localStorage provides no such protection.
Medium because exploitation requires either a successful XSS attack or a malicious third-party script to be running on the page, but either condition is common.
Replace localStorage token storage with HttpOnly cookies set by the server. The browser sends HttpOnly cookies automatically with requests and JavaScript cannot read them at all:
// Remove this pattern entirely:
localStorage.setItem('token', jwtToken)
// Set the token server-side on the login response:
res.setHeader('Set-Cookie',
`session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800`
)
// For non-sensitive UI data, localStorage is still fine:
localStorage.setItem('displayName', user.name)
For React SPAs that need to make authenticated API calls, configure your HTTP client to include credentials (credentials: 'include' in fetch, withCredentials: true in Axios) so cookie-based auth works across your origin.
ID: saas-authentication.session-management.auth-state-not-localstorage
Severity: medium
What to look for: Search the codebase for localStorage.setItem or localStorage.getItem calls that reference tokens, session data, or user credentials. Check client-side auth state management code, login components, and any client-side API clients. Note: storing minimal non-sensitive user display data (name, email for UI) in localStorage is acceptable — only auth tokens and session credentials are at risk. Count all instances found and enumerate each.
Pass criteria: JWT tokens, session tokens, refresh tokens, and API keys are not stored in localStorage. Auth state is maintained via HttpOnly cookies or in-memory state only. At least 1 implementation must be confirmed.
Fail criteria: Bearer tokens, JWT access tokens, refresh tokens, or API keys are stored in localStorage or sessionStorage.
Skip (N/A) when: No client-side auth state management found — fully server-rendered application with no client-side auth token handling. Signal: no client-side JavaScript that accesses auth tokens.
Detail on fail: "JWT access token stored in localStorage at src/lib/auth-client.ts line 42 — accessible to any JavaScript on the page" or "Refresh token written to localStorage in useAuth hook".
Remediation: localStorage is readable by any JavaScript running on your page, including third-party scripts and XSS payloads. Auth tokens stored there are trivially stolen:
// Wrong: token in localStorage
localStorage.setItem('token', jwtToken)
// Correct: rely on HttpOnly cookies (set server-side)
// The browser sends them automatically with requests
// JS cannot read them at all
// If you need client-accessible auth state, store only
// non-sensitive info like user ID or display name:
localStorage.setItem('userId', user.id) // Acceptable for UI
Move token storage to HttpOnly cookies. For SPAs that need to make authenticated API calls, use cookie-based auth (the server sets the cookie, the browser sends it automatically) rather than manually attaching tokens.