Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.
Why this matters
Any XSS that lands on the page can exfiltrate the token via localStorage.getItem(...), and unlike HttpOnly cookies, there is no browser-side mitigation.
Store auth tokens in an HttpOnly; Secure; SameSite=Strict cookie set by the server, or in memory only. sessionStorage is no safer against XSS. It's the same attacker capability.
OWASP ASVS V3.4: tokens must not be stored where untrusted scripts can read them.
VULNERABLE
vulnerable.ts
declare const localStorage: Storage;declare const sessionStorage: Storage;export function storeBad(token: string) { // ruleid: auth.jwt.localstorage localStorage.setItem('access_token', token);}export function storeBad2(refresh: string) { // ruleid: auth.jwt.localstorage localStorage.setItem('refresh_token', refresh);}export function storeBad3(jwtVal: string) { // ruleid: auth.jwt.localstorage localStorage['authToken'] = jwtVal;}export function storeBad4(token: string) { // ruleid: auth.jwt.localstorage window.localStorage.setItem('jwt', token);}export function storeBad5(token: string) { // ruleid: auth.jwt.localstorage -- sessionStorage is no safer against XSS sessionStorage.setItem('access_token', token);}const TOKEN_STORAGE_KEY = 'app.session';export function storeBad6(token: string) { // ruleid: auth.jwt.localstorage -- key is a variable, but the value is the token localStorage.setItem(TOKEN_STORAGE_KEY, token);}
SAFE
safe.ts
declare const localStorage: Storage;// ok: auth.jwt.localstorage -- not auth-relatedexport function storeFontPref(font: string) { localStorage.setItem('font_preference', font);}// These keys contain auth-word substrings but are NOT token storage —// they must not be flagged (regression guards for the substring FP).export function storeUiPrefs(name: string, n: number) { // ok: auth.jwt.localstorage -- "author" contains "auth" localStorage.setItem('author_filter', name); // ok: auth.jwt.localstorage -- "auto_refresh" contains "refresh" localStorage.setItem('auto_refresh', 'true'); // ok: auth.jwt.localstorage -- "access_count" contains "access" localStorage.setItem('access_count', String(n)); // ok: auth.jwt.localstorage -- "session" UI flag, not a token localStorage.setItem('sidebar_session_collapsed', '1');}// ok: auth.jwt.localstorage -- tokens stay in memory, not localStoragelet inMemoryToken: string | null = null;export function setToken(t: string) { inMemoryToken = t;}export function getToken() { return inMemoryToken;}
Suppressing this rule
If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.