October 12, 2026

The LocalStorage Security Paradox: A Comprehensive Field Study on Frontend Credential Storage

the-localstorage-security-paradox-a-comprehensive-field-study-on-frontend-credential-storage

the-localstorage-security-paradox-a-comprehensive-field-study-on-frontend-credential-storage

Main Facts

For years, the cardinal rule of frontend web security has been relentlessly repeated across developer forums, security blogs, and documentation pages: “Never put a JSON Web Token (JWT) in localStorage.”

Yet, despite this ubiquity as an axiom of modern web development, the actual prevalence of this practice in production codebases has remained largely unmeasured—until now. A new empirical field study analyzing 258,117 source files across 189 public repositories has sought to quantify the gap between frontend security theory and engineering reality.

The findings challenge preconceived notions about a widespread security epidemic, revealing that only a fraction of codebases actually violate the rule. Out of the 1,423 repositories and files that touch web storage APIs, precisely 13 codebases were found to be actively storing credentials. Among them, 11 contained bearer tokens.

Most surprisingly, more than half of these violations—seven out of the thirteen—were traced back to public-sector and government digital services. The remaining six non-governmental repositories comprised developer tooling, internal sandboxes, playground environments, and developer-focused consoles.

Rather than uncovering a systemic, industry-wide catastrophe, the audit highlights a specific vulnerability pattern: government frontends, often built under rigid procurement deadlines by rotating contractors, frequently inherit boilerplate configurations from standard OpenID Connect (OIDC) tutorials. Those tutorials, designed for simplicity rather than hardened production security, routinely default to placing tokens directly into localStorage.


Chronology of the Audit

To understand how these vulnerabilities were identified and cataloged, it is necessary to examine the step-by-step methodology deployed during the field study:

  1. Corpus Selection and Initial Scans: The researcher gathered a targeted corpus of 189 public repositories, encompassing a massive baseline of 258,117 individual source files.
  2. Isolating Web Storage Usage: Using automated text-matching and AST (Abstract Syntax Tree) parsing, the study filtered down to files interacting with native web storage interfaces (localStorage and sessionStorage). This narrowed the active denominator to 1,423 touchpoints.
  3. Deploying the Custom Linter: The custom ESLint rule, no-jwt-in-storage (part of the eslint-plugin-browser-security package), was executed across the corpus to flag potential security violations based on method calls and variable heuristics.
  4. Initial Flagging and False Positive Filtration: The linter initially flagged 19 repositories (comprising 39 distinct files). Recognizing the inherent noise in static analysis, the researcher undertook manual code reviews for every single flagged file to establish ground truth.
  5. Deduplication and Contextual Analysis: Manual audits eliminated six false positives where variable names mimicked sensitive tokens but held benign data. The remaining 13 repositories were cataloged, their specific storage mechanisms verified, and their organizational affiliations mapped.

Supporting Data and Technical Breakdown

While the raw headline figure—13 offending repositories out of 189—suggests a relatively low violation rate of roughly 7%, the granular data reveals critical nuances regarding what is being stored and how.

The Offending Repositories

The audit unmasked specific high-profile public-sector and institutional repositories storing credentials in client-side web storage:

  • City of Helsinki (kukkuu-admin): localStorage.setItem(API_TOKEN, user.access_token)
  • UK Office for National Statistics (eq-author-app): localStorage.setItem("accessToken", user.ra)
  • US General Services Administration (srt-ui): localStorage.setItem('token', data.token)
  • Government of Canada (cds-snc): localStorage.setItem("token", response.access_token)
  • French Digital Public Service Incubator (betagouv — 2 instances): xsrfToken, NATIVE_TOKEN_KEY
  • Province of British Columbia (sso-requests): sessionStorage.setItem(TOKEN_SESSION, …)

The remaining six non-governmental repositories included cloudflare-os, headlamp, mozilla_fxa, solid-playground, wix_skills, and a Twilio-associated application storing a passcode.

Precision, False Positives, and the "Missing" Rule

Static analysis tools are notoriously plagued by false positives, and this study was no exception. The custom ESLint rule achieved a 68% precision rate, flagging 19 repositories of which 6 proved to be false positives. These false alarms shared a common flaw: matching identifier names rather than proving actual credential handling. Examples included:

  • localStorage.setItem("sessionId", sessionId); (A benign demo session identifier)
  • sessionStorage.setItem('app_authServer', config); (A static server URL configuration string)
  • localStorage.setItem(AUTH_STATUS_KEY, 'success'); (Storing a literal status string)

Other false positives included co-browse session descriptors, telemetry session markers, sandbox IDs, and example user objects.

Notably, another rule included in the suite—no-cookie-auth-tokens—fired zero times across all 1,423 files, demonstrating that static analysis rules must be rigorously benchmarked against ground truth to avoid generating misleading metrics.


Official Responses and Threat Analysis

The implications of storing tokens in web storage depend heavily on the type of token and the storage mechanism employed. A superficial reading of the codebases can easily lead to misdiagnoses.

Nuances in the Findings

Manual inspection of the 13 flagged repositories revealed two important caveats:

  1. SessionStorage vs. LocalStorage: British Columbia’s sso-requests repository utilized sessionStorage rather than localStorage. While both present identical exposure risks to Cross-Site Scripting (XSS) attacks, sessionStorage tokens do not persist across browser restarts, dying immediately when the tab is closed.
  2. Non-Bearer Credentials: France’s betagouv repository stored an XSRF token rather than a bearer authentication credential. XSRF tokens are fundamentally designed to be readable by the client-side page and do not grant direct API access if exfiltrated in isolation.

The Real Danger: Refresh Tokens

The core argument against web storage rests on trust boundaries. Web storage is a shared trust boundary accessible by every script executing on the page. If an application suffers from an XSS vulnerability—whether through a compromised npm dependency, an injection flaw, or a malicious browser extension—any script can synchronously read the contents of localStorage or sessionStorage.

While a short-lived access token (expiring in minutes) presents a limited window of risk, two of the government services analyzed—Helsinki and the UK ONS—were found storing refresh tokens alongside access tokens in web storage. Because refresh tokens are long-lived credentials capable of generating new access tokens indefinitely, exposing them in a synchronous DOM sink drastically amplifies the blast radius of any client-side compromise.


Broader Implications for Web Security

The findings of this field study offer several vital takeaways for software engineers, security architects, and government procurement agencies:

1. The Tutorial Inheritance Problem

Public-sector and enterprise frontends are frequently outsourced to external contractors operating under tight deadlines. Under pressure to deliver functional prototypes rapidly, developers often lean on default OIDC tutorials and starter kits. If mainstream authentication tutorials lazily default to localStorage for ease of implementation, that insecure pattern propagates downstream into government portals, municipal dashboards, and civic tools.

2. Moving Beyond Dogma

The security industry often treats frontend advice in absolute binaries. However, as the study proves, context matters. Developer consoles, sandboxes, and local playgrounds (solid-playground, headlamp) face entirely different threat models than public-facing consumer applications. While best practices still discourage web storage for sensitive tokens everywhere, security tooling must account for context to maintain high signal-to-noise ratios.

3. Implementing Automated Governance

For teams wishing to audit their own codebases, integrating custom static analysis rules into CI/CD pipelines via ESLint plugins (eslint-plugin-browser-security) provides an automated safety net.

// eslint.config.mjs — after running `npm i -D eslint-plugin-browser-security`
import browser from "eslint-plugin-browser-security";

export default [
  
    files: ["**/*.ts,tsx,js,jsx"],
    plugins:  "browser-security": browser ,
    rules:  "browser-security/no-jwt-in-storage": "error" ,
  ,
];

Ultimately, automated linters are only the first line of defense. As this study demonstrates, ground truth requires human verification. Developers must audit their dependencies, scrutinize their variable declarations, and ensure that long-lived credentials—especially refresh tokens—are kept entirely out of client-side web storage mechanisms.