no-sensitive-localstorage
Detects storage of sensitive data (tokens, passwords, PII) in localStorage. This rule is part of [`eslint-plugin-browser-security`](https://www.npmjs.com/packag
Keywords: localStorage, sessionStorage, tokens, CWE-922, security, XSS, sensitive data
Detects storage of sensitive data (tokens, passwords, PII) in localStorage. This rule is part of eslint-plugin-browser-security.
⚠️ This rule errors by default in the recommended config.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-922 (Insecure Storage of Sensitive Information) |
| Severity | 🔴 High |
| Auto-Fix | ❌ No (requires architecture change) |
| Category | Security |
| Best For | SPAs, frontend apps handling authentication |
Vulnerability and Risk
Vulnerability: localStorage is accessible to any JavaScript running on the page. If an XSS vulnerability exists, attackers can steal tokens.
Risk:
- XSS attacks can steal all localStorage data
- Tokens persist beyond session (unlike cookies)
- No protection against malicious browser extensions
- Shared across tabs (potential for leakage)
Why localStorage is Dangerous for Tokens
Examples
❌ Incorrect
// Storing tokens - VULNERABLE to XSS
localStorage.setItem('token', jwtToken);
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
// Storing sensitive user data - VULNERABLE
localStorage.setItem(
'user',
JSON.stringify({
email: 'user@example.com',
ssn: '123-45-6789',
password: 'secret',
}),
);
// Session storage has same issues
sessionStorage.setItem('authToken', token);✅ Correct
// Use httpOnly cookies for tokens (set by server)
// Server response:
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// Store only non-sensitive preferences
localStorage.setItem('theme', 'dark');
localStorage.setItem('language', 'en');
// For tokens that must be in JS, use memory-only storage
class TokenStore {
#token = null; // Private field, not persisted
setToken(token) {
this.#token = token;
}
getToken() {
return this.#token;
}
}
// Or use a closure
const tokenStore = (() => {
let token = null;
return {
set: (t) => {
token = t;
},
get: () => token,
};
})();Options
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | false | Allow in test files |
sensitiveKeys | string[] | ['token', 'password', 'secret', 'key', 'auth', 'credential', 'ssn', 'credit'] | Keys considered sensitive |
{
"rules": {
"browser-security/no-sensitive-localstorage": [
"error",
{
"sensitiveKeys": ["token", "password", "apiKey", "secret"]
}
]
}
}Best Practices
1. Use httpOnly Cookies for Tokens
// Backend sets the cookie
res.cookie('accessToken', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000,
});
// Frontend just makes requests (cookie sent automatically)
fetch('/api/protected', {
credentials: 'include',
});2. Use In-Memory Storage
// Token disappears on page refresh (good for security)
let accessToken = null;
function setToken(token) {
accessToken = token;
}
function getToken() {
return accessToken;
}Comparison: Storage Options
| Method | XSS Safe? | Persists? | Use For |
|---|---|---|---|
| httpOnly Cookie | ✅ Yes | ✅ Yes | Auth tokens |
| Memory variable | ✅ Yes | ❌ No | Temporary tokens |
| sessionStorage | ❌ No | Tab only | Non-sensitive data |
| localStorage | ❌ No | ✅ Yes | Only user preferences |
Related Rules
no-innerhtml- Prevent XSS that could access localStoragerequire-postmessage-origin-check- Cross-origin security
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Dynamic Key Names
Why: Computed key names are not analyzed.
// ❌ NOT DETECTED - Dynamic key
const key = 'accessToken';
localStorage.setItem(key, value);Mitigation: Use consistent naming. Configure sensitiveKeys pattern.
Nested Sensitive Data
Why: Sensitive data inside objects may not be detected.
// ❌ NOT DETECTED - Nested sensitive data
localStorage.setItem('user', JSON.stringify({ token: jwt }));Mitigation: Never store objects containing tokens.
Wrapper Functions
Why: Custom storage wrappers are not recognized.
// ❌ NOT DETECTED - Wrapper function
storageHelper.save('token', jwt); // Uses localStorage internallyMitigation: Apply rule to wrapper implementations.
IndexedDB
Why: Different API not covered by this rule.
// ❌ NOT DETECTED - IndexedDB
db.put({ token: jwt });Mitigation: Use no-sensitive-indexeddb rule.