Skip to content

auth.jwt.localstorage

A JWT (or other auth token) is being written to localStorage. Any

OAuthLint idAUTH-JWT-005
SeverityWARNING
LLM prevalenceHIGH
CWECWE-922
OWASPAPI8:2023
Languagesjavascript, typescript

Why this matters

A JWT (or other auth token) is being written to localStorage. 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

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

ts
declare const localStorage: Storage;

// ok: auth.jwt.localstorage -- not auth-related
export 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 localStorage
let inMemoryToken: string | null = null;
export function setToken(t: string) {
  inMemoryToken = t;
}
export function getToken() {
  return inMemoryToken;
}

Suppressing this rule (when you really must)

ts
// oauthlint-disable-next-line auth.jwt.localstorage -- <reason>
thisLineWouldOtherwiseTriggerTheRule();

Disable directives are line-scoped by design — wholesale silencing of a rule across the codebase is intentionally not supported, because the next reviewer needs to see exactly which lines opted out.

References

Released under the MIT License · powered by Semgrep