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
The key is token-named (access_token, refresh_token, id_token, …), and web storage is exposed to every script on the page, so any XSS, including a compromised third-party dependency, can exfiltrate the token via getItem(...). There is no browser-side mitigation, unlike HttpOnly cookies.
Keep access/refresh/id tokens in memory only, or have the server issue them in a Secure; HttpOnly; SameSite=Strict cookie. sessionStorage is no safer than localStorage against XSS: it grants the same attacker capability.
See CWE-922: Insecure Storage of Sensitive Information.
VULNERABLE
vulnerable.ts
declare const localStorage: Storage;declare const sessionStorage: Storage;export function storeAccess(token: string) { // ruleid: auth.oauth.token-in-localstorage localStorage.setItem('access_token', token);}export function storeRefresh(rt: string) { // ruleid: auth.oauth.token-in-localstorage -- sessionStorage is no safer against XSS sessionStorage.setItem('refresh_token', rt);}export function storeId(idToken: string) { // ruleid: auth.oauth.token-in-localstorage localStorage.setItem('id_token', idToken);}export function storeAccessCamel(token: string) { // ruleid: auth.oauth.token-in-localstorage -- camelCase accessToken key localStorage.setItem('accessToken', token);}export function storeRefreshCamel(rt: string) { // ruleid: auth.oauth.token-in-localstorage sessionStorage.setItem('refreshToken', rt);}export function storeIdProp(t: string) { // ruleid: auth.oauth.token-in-localstorage -- property-form write localStorage.idToken = t;}export function storeAccessBracket(t: string) { // ruleid: auth.oauth.token-in-localstorage -- bracket-form write localStorage['access_token'] = t;}export function storeViaWindow(token: string) { // ruleid: auth.oauth.token-in-localstorage window.localStorage.setItem('id_token', token);}
SAFE
safe.ts
declare const localStorage: Storage;declare const sessionStorage: Storage;// ok: auth.oauth.token-in-localstorage -- non-token key, must not matchexport function storeTheme() { localStorage.setItem('theme', 'dark');}// ok: auth.oauth.token-in-localstorage -- arbitrary UI prefs are not tokensexport function storeUiPrefs(name: string) { localStorage.setItem('username', name); localStorage.setItem('sidebar_collapsed', '1'); sessionStorage.setItem('last_route', '/dashboard');}// ok: auth.oauth.token-in-localstorage -- token kept in memory, never persistedlet inMemoryAccessToken: string | null = null;export function setAccessToken(t: string) { inMemoryAccessToken = t;}export function getAccessToken() { return inMemoryAccessToken;}// ok: auth.oauth.token-in-localstorage -- server sets a Secure HttpOnly cookieexport function buildAuthCookie(token: string): string { return `session=${token}; Secure; HttpOnly; SameSite=Strict`;}
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.