Browser Storage Security and Limits: A Developer's Guide
A practical Fusebox guide to browser storage security and limits.
Browser Storage Security and Limits: A Developer's Guide
Browser storage has evolved from simple cookies to sophisticated APIs like IndexedDB. Yet with great power comes great responsibility—and risk. In 2021, a major news website accidentally exposed user preferences and reading history through improperly secured localStorage, affecting millions. Understanding browser storage security isn't optional—it's critical for protecting user data.
Why Browser Storage Security Matters
Modern web applications store everything from user preferences to authentication tokens in browser storage. A single XSS vulnerability can turn these storage mechanisms into data goldmines for attackers. With storage limits varying wildly across browsers and devices, developers must balance functionality with security and reliability.
Comprehensive Storage Analyzer
Start with this complete browser storage analyzer:
// Comprehensive Browser Storage Analyzer
const StorageAnalyzer = {
// Analyze all storage mechanisms
async analyzeAllStorage() {
const analysis = {
timestamp: new Date().toISOString(),
summary: {},
details: {},
security: {},
limits: {},
recommendations: []
};
// Analyze each storage type
analysis.details.cookies = this.analyzeCookies();
analysis.details.localStorage = this.analyzeLocalStorage();
analysis.details.sessionStorage = this.analyzeSessionStorage();
analysis.details.indexedDB = await this.analyzeIndexedDB();
analysis.details.webSQL = this.analyzeWebSQL();
analysis.details.cacheStorage = await this.analyzeCacheStorage();
// Test storage limits
analysis.limits = await this.testStorageLimits();
// Security analysis
analysis.security = this.analyzeSecurityIssues();
// Generate summary
analysis.summary = this.generateSummary(analysis);
// Generate recommendations
analysis.recommendations = this.generateRecommendations(analysis);
return analysis;
},
// Analyze cookies
analyzeCookies() {
const cookies = document.cookie.split(';').filter(c => c.trim());
const analysis = {
count: cookies.length,
totalSize: document.cookie.length,
cookies: [],
security: {
httpOnly: 0,
secure: 0,
sameSite: 0
}
};
cookies.forEach(cookie => {
const [name, value] = cookie.trim().split('=');
const cookieData = {
name,
size: cookie.length,
value: value ? value.substring(0, 20) + '...' : '',
flags: []
};
// Note: Can't detect httpOnly, secure, sameSite from JavaScript
// These would need to be checked via HTTP headers
analysis.cookies.push(cookieData);
});
// Check for sensitive data patterns
analysis.sensitiveData = this.checkSensitiveData(document.cookie);
return analysis;
},
// Analyze localStorage
analyzeLocalStorage() {
const analysis = {
count: localStorage.length,
totalSize: 0,
items: [],
security: {
encryptedItems: 0,
sensitiveData: []
}
};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
const size = key.length + value.length;
analysis.totalSize += size;
const item = {
key,
size,
type: this.detectDataType(value),
encrypted: this.isLikelyEncrypted(value)
};
if (item.encrypted) {
analysis.security.encryptedItems++;
}
// Check for sensitive data
const sensitive = this.checkSensitiveData(key + value);
if (sensitive.length > 0) {
analysis.security.sensitiveData.push({
key,
patterns: sensitive
});
}
analysis.items.push(item);
}
return analysis;
},
// Analyze sessionStorage
analyzeSessionStorage() {
const analysis = {
count: sessionStorage.length,
totalSize: 0,
items: [],
security: {
encryptedItems: 0,
sensitiveData: []
}
};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const value = sessionStorage.getItem(key);
const size = key.length + value.length;
analysis.totalSize += size;
const item = {
key,
size,
type: this.detectDataType(value),
encrypted: this.isLikelyEncrypted(value)
};
if (item.encrypted) {
analysis.security.encryptedItems++;
}
analysis.items.push(item);
}
return analysis;
},
// Analyze IndexedDB
async analyzeIndexedDB() {
const analysis = {
databases: [],
totalSize: 0,
security: {
encrypted: 0,
unencrypted: 0
}
};
if (!window.indexedDB) {
analysis.supported = false;
return analysis;
}
try {
// Get database names (only works in some browsers)
const databases = await indexedDB.databases?.() || [];
for (const dbInfo of databases) {
const db = await this.openDatabase(dbInfo.name);
const dbAnalysis = {
name: dbInfo.name,
version: db.version,
objectStores: []
};
const storeNames = Array.from(db.objectStoreNames);
for (const storeName of storeNames) {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const count = await this.promisify(store.count());
dbAnalysis.objectStores.push({
name: storeName,
keyPath: store.keyPath,
autoIncrement: store.autoIncrement,
recordCount: count,
indexes: Array.from(store.indexNames)
});
}
db.close();
analysis.databases.push(dbAnalysis);
}
} catch (error) {
analysis.error = error.message;
}
return analysis;
},
// Analyze WebSQL (deprecated but still present)
analyzeWebSQL() {
const analysis = {
supported: !!window.openDatabase,
databases: []
};
if (analysis.supported) {
analysis.warning = 'WebSQL is deprecated - migrate to IndexedDB';
}
return analysis;
},
// Analyze Cache Storage
async analyzeCacheStorage() {
const analysis = {
supported: 'caches' in window,
caches: [],
totalSize: 0
};
if (!analysis.supported) {
return analysis;
}
try {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
const cacheInfo = {
name,
entries: requests.length,
urls: requests.map(r => r.url)
};
analysis.caches.push(cacheInfo);
}
} catch (error) {
analysis.error = error.message;
}
return analysis;
},
// Test storage limits
async testStorageLimits() {
const limits = {
localStorage: await this.testLocalStorageLimit(),
sessionStorage: await this.testSessionStorageLimit(),
indexedDB: await this.testIndexedDBLimit(),
cookies: this.getCookieLimit()
};
// Estimate available storage
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
limits.quota = {
usage: estimate.usage,
quota: estimate.quota,
percentUsed: (estimate.usage / estimate.quota * 100).toFixed(2)
};
} catch (error) {
limits.quota = { error: error.message };
}
}
return limits;
},
// Test localStorage limit
async testLocalStorageLimit() {
const testKey = '__storage_test__';
let size = 0;
const chunkSize = 1024 * 100; // 100KB chunks
try {
// Clear test key first
localStorage.removeItem(testKey);
while (true) {
const chunk = 'x'.repeat(chunkSize);
const currentValue = localStorage.getItem(testKey) || '';
localStorage.setItem(testKey, currentValue + chunk);
size += chunkSize;
// Safety limit
if (size > 50 * 1024 * 1024) { // 50MB
break;
}
}
} catch (e) {
// Quota exceeded
} finally {
localStorage.removeItem(testKey);
}
return {
approximate: size,
formatted: this.formatBytes(size)
};
},
// Test sessionStorage limit
async testSessionStorageLimit() {
const testKey = '__storage_test__';
let size = 0;
const chunkSize = 1024 * 100; // 100KB chunks
try {
sessionStorage.removeItem(testKey);
while (true) {
const chunk = 'x'.repeat(chunkSize);
const currentValue = sessionStorage.getItem(testKey) || '';
sessionStorage.setItem(testKey, currentValue + chunk);
size += chunkSize;
if (size > 50 * 1024 * 1024) { // 50MB safety limit
break;
}
}
} catch (e) {
// Quota exceeded
} finally {
sessionStorage.removeItem(testKey);
}
return {
approximate: size,
formatted: this.formatBytes(size)
};
},
// Test IndexedDB limit
async testIndexedDBLimit() {
// IndexedDB limits are much larger and vary by browser
// This is a simplified check
return {
note: 'IndexedDB limits vary by browser and available disk space',
typical: '50% of free disk space',
minimum: '100MB to 1GB depending on browser'
};
},
// Get cookie limits
getCookieLimit() {
return {
maxCookies: 'Varies by browser (typically 50-300 per domain)',
maxSize: '4KB per cookie',
totalSize: 'Varies (typically 10KB-100KB total)'
};
},
// Security analysis
analyzeSecurityIssues() {
const issues = [];
const score = 100;
// Check for authentication tokens in localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
if (key.toLowerCase().includes('token') ||
key.toLowerCase().includes('auth') ||
value.includes('bearer') ||
this.looksLikeJWT(value)) {
issues.push({
severity: 'high',
type: 'auth-token-in-localstorage',
message: 'Authentication tokens should not be stored in localStorage',
key: key,
recommendation: 'Use httpOnly cookies or sessionStorage for sensitive tokens'
});
}
}
// Check for PII in storage
const storageContent = JSON.stringify(localStorage) + JSON.stringify(sessionStorage);
const piiPatterns = this.checkSensitiveData(storageContent);
if (piiPatterns.length > 0) {
issues.push({
severity: 'medium',
type: 'pii-in-storage',
message: 'Potentially sensitive data found in storage',
patterns: piiPatterns,
recommendation: 'Encrypt sensitive data before storing'
});
}
// Check for large unencrypted data
const largeUnencrypted = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
if (value.length > 10000 && !this.isLikelyEncrypted(value)) {
largeUnencrypted.push(key);
}
}
if (largeUnencrypted.length > 0) {
issues.push({
severity: 'low',
type: 'large-unencrypted-data',
message: 'Large unencrypted data found',
keys: largeUnencrypted,
recommendation: 'Consider encrypting large data blobs'
});
}
return {
score: Math.max(0, score - (issues.filter(i => i.severity === 'high').length * 20)),
issues
};
},
// Helper functions
detectDataType(value) {
try {
JSON.parse(value);
return 'json';
} catch (e) {
if (this.looksLikeJWT(value)) return 'jwt';
if (this.isLikelyEncrypted(value)) return 'encrypted';
if (value.match(/^\d+$/)) return 'number';
if (value.match(/^\d{4}-\d{2}-\d{2}/)) return 'date';
return 'string';
}
},
isLikelyEncrypted(value) {
// Simple heuristic for encrypted data
if (value.length < 20) return false;
// Check for high entropy (lots of different characters)
const uniqueChars = new Set(value).size;
const entropy = uniqueChars / value.length;
// Check for base64 pattern
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
return entropy > 0.7 || base64Pattern.test(value);
},
looksLikeJWT(value) {
return /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(value);
},
checkSensitiveData(text) {
const patterns = [
{ name: 'email', regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ },
{ name: 'phone', regex: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/ },
{ name: 'ssn', regex: /\b\d{3}-\d{2}-\d{4}\b/ },
{ name: 'credit-card', regex: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/ },
{ name: 'api-key', regex: /api[_-]?key[\s:="']+[\w\-]+/i },
{ name: 'password', regex: /password[\s:="']+[\w\-]+/i }
];
const found = [];
patterns.forEach(pattern => {
if (pattern.regex.test(text)) {
found.push(pattern.name);
}
});
return found;
},
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
openDatabase(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
generateSummary(analysis) {
const summary = {
totalItems: 0,
totalSize: 0,
byType: {}
};
// Count items and size
Object.entries(analysis.details).forEach(([type, data]) => {
if (data.count !== undefined) {
summary.totalItems += data.count;
summary.byType[type] = {
count: data.count,
size: data.totalSize || 0
};
summary.totalSize += data.totalSize || 0;
}
});
summary.formattedSize = this.formatBytes(summary.totalSize);
summary.securityScore = analysis.security.score;
return summary;
},
generateRecommendations(analysis) {
const recommendations = [];
// Security recommendations
if (analysis.security.issues.length > 0) {
analysis.security.issues.forEach(issue => {
if (issue.severity === 'high') {
recommendations.push({
priority: 'critical',
action: issue.recommendation,
reason: issue.message
});
}
});
}
// Storage optimization
if (analysis.summary.totalSize > 5 * 1024 * 1024) { // 5MB
recommendations.push({
priority: 'medium',
action: 'Implement storage cleanup strategy',
reason: 'Large amount of data in browser storage'
});
}
// Cookie recommendations
if (analysis.details.cookies.count > 20) {
recommendations.push({
priority: 'low',
action: 'Review and consolidate cookies',
reason: 'High number of cookies can impact performance'
});
}
return recommendations;
}
};
// Run analysis
StorageAnalyzer.analyzeAllStorage().then(console.log);
Storage Security Scanner
Comprehensive security scanner for browser storage:
// Storage Security Scanner
const StorageSecurityScanner = {
// Scan for security vulnerabilities
async scanSecurity() {
const results = {
vulnerabilities: [],
risks: {
high: 0,
medium: 0,
low: 0
},
recommendations: [],
score: 100
};
// Scan each storage type
this.scanCookieSecurity(results);
this.scanLocalStorageSecurity(results);
this.scanSessionStorageSecurity(results);
await this.scanIndexedDBSecurity(results);
this.scanCrossOriginRisks(results);
// Calculate final score
results.score = Math.max(0, 100 -
(results.risks.high * 20) -
(results.risks.medium * 10) -
(results.risks.low * 5)
);
return results;
},
// Scan cookie security
scanCookieSecurity(results) {
const cookies = document.cookie.split(';').filter(c => c.trim());
cookies.forEach(cookie => {
const [name, value] = cookie.trim().split('=');
// Check for session cookies without secure attributes
if (name.toLowerCase().includes('session') ||
name.toLowerCase().includes('auth')) {
// Note: Can't check httpOnly or secure from JavaScript
results.vulnerabilities.push({
type: 'insecure-session-cookie',
severity: 'high',
storage: 'cookie',
key: name,
issue: 'Session cookie may lack security attributes',
recommendation: 'Set httpOnly, secure, and sameSite attributes'
});
results.risks.high++;
}
// Check for sensitive data in cookies
if (this.containsSensitiveData(value)) {
results.vulnerabilities.push({
type: 'sensitive-data-in-cookie',
severity: 'medium',
storage: 'cookie',
key: name,
issue: 'Potentially sensitive data in cookie',
recommendation: 'Encrypt sensitive cookie values'
});
results.risks.medium++;
}
});
},
// Scan localStorage security
scanLocalStorageSecurity(results) {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
// Check for authentication tokens
if (this.isAuthenticationToken(key, value)) {
results.vulnerabilities.push({
type: 'auth-token-in-localstorage',
severity: 'high',
storage: 'localStorage',
key: key,
issue: 'Authentication token in localStorage is vulnerable to XSS',
recommendation: 'Use httpOnly cookies for auth tokens',
codeExample: `
// Instead of:
localStorage.setItem('authToken', token);
// Use httpOnly cookie:
// Set on server with httpOnly flag`
});
results.risks.high++;
}
// Check for unencrypted sensitive data
if (this.containsSensitiveData(value) && !this.isEncrypted(value)) {
results.vulnerabilities.push({
type: 'unencrypted-sensitive-data',
severity: 'medium',
storage: 'localStorage',
key: key,
issue: 'Sensitive data stored without encryption',
recommendation: 'Encrypt data before storing',
codeExample: `
// Use encryption:
const encrypted = await crypto.subtle.encrypt(
algorithm,
key,
encoder.encode(sensitiveData)
);
localStorage.setItem('data', btoa(String.fromCharCode(...new Uint8Array(encrypted))));`
});
results.risks.medium++;
}
// Check for PII
if (this.containsPII(value)) {
results.vulnerabilities.push({
type: 'pii-in-storage',
severity: 'medium',
storage: 'localStorage',
key: key,
issue: 'Personally Identifiable Information in storage',
recommendation: 'Avoid storing PII or encrypt it'
});
results.risks.medium++;
}
}
},
// Scan sessionStorage security
scanSessionStorageSecurity(results) {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const value = sessionStorage.getItem(key);
// SessionStorage is better for sensitive data than localStorage
// but still check for unencrypted sensitive data
if (this.containsCreditCard(value) || this.containsSSN(value)) {
results.vulnerabilities.push({
type: 'highly-sensitive-data',
severity: 'high',
storage: 'sessionStorage',
key: key,
issue: 'Highly sensitive data (CC/SSN) in storage',
recommendation: 'Never store credit cards or SSNs client-side'
});
results.risks.high++;
}
}
},
// Scan IndexedDB security
async scanIndexedDBSecurity(results) {
if (!window.indexedDB) return;
try {
const databases = await indexedDB.databases?.() || [];
if (databases.length > 0) {
// General IndexedDB security note
results.vulnerabilities.push({
type: 'indexeddb-usage',
severity: 'low',
storage: 'IndexedDB',
issue: 'IndexedDB data is accessible to any script',
recommendation: 'Encrypt sensitive data in IndexedDB',
databases: databases.map(db => db.name)
});
results.risks.low++;
}
} catch (error) {
// IndexedDB.databases() not supported
}
},
// Scan cross-origin risks
scanCrossOriginRisks(results) {
// Check for cross-origin data
const storageKeys = [];
for (let i = 0; i < localStorage.length; i++) {
storageKeys.push(localStorage.key(i));
}
// Look for keys that might indicate cross-origin data
const crossOriginPatterns = ['iframe', 'embed', 'widget', 'third-party'];
storageKeys.forEach(key => {
if (crossOriginPatterns.some(pattern => key.toLowerCase().includes(pattern))) {
results.vulnerabilities.push({
type: 'potential-cross-origin-data',
severity: 'low',
storage: 'localStorage',
key: key,
issue: 'Potential cross-origin data sharing',
recommendation: 'Validate origin before trusting data'
});
results.risks.low++;
}
});
},
// Helper methods
isAuthenticationToken(key, value) {
const tokenPatterns = ['token', 'auth', 'jwt', 'bearer', 'session'];
const keyMatch = tokenPatterns.some(pattern =>
key.toLowerCase().includes(pattern)
);
// Check if value looks like a token
const isJWT = /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(value);
const isBearerToken = value.toLowerCase().startsWith('bearer ');
const isLongRandomString = value.length > 20 && /^[A-Za-z0-9\-_]+$/.test(value);
return keyMatch || isJWT || isBearerToken || isLongRandomString;
},
containsSensitiveData(value) {
const patterns = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // Email
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, // Phone
/\b\d{3}-\d{2}-\d{4}\b/, // SSN
/password/i,
/api[_-]?key/i,
/secret/i
];
return patterns.some(pattern => pattern.test(value));
},
containsPII(value) {
// Check for common PII patterns
const piiPatterns = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/, // Email
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, // Phone
/\b\d{5}(?:[-\s]\d{4})?\b/, // ZIP code
/\b(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12]\d|3[01])\/(?:19|20)\d{2}\b/ // Birthdate
];
return piiPatterns.some(pattern => pattern.test(value));
},
containsCreditCard(value) {
// Basic credit card pattern
const ccPattern = /\b(?:\d{4}[\s-]?){3}\d{4}\b/;
return ccPattern.test(value);
},
containsSSN(value) {
const ssnPattern = /\b\d{3}-\d{2}-\d{4}\b/;
return ssnPattern.test(value);
},
isEncrypted(value) {
// Simple heuristic for encrypted data
if (value.length < 20) return false;
// Check for high entropy
const uniqueChars = new Set(value).size;
const entropy = uniqueChars / value.length;
// Check for base64 or hex patterns
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
const hexPattern = /^[0-9a-fA-F]+$/;
return entropy > 0.7 || base64Pattern.test(value) || hexPattern.test(value);
}
};
// Run security scan
StorageSecurityScanner.scanSecurity().then(console.log);
Storage Encryption Helper
Implement secure storage with encryption:
// Secure Storage Implementation
class SecureStorage {
constructor() {
this.algorithm = { name: 'AES-GCM', length: 256 };
this.keyUsages = ['encrypt', 'decrypt'];
}
// Generate encryption key
async generateKey() {
return await crypto.subtle.generateKey(
this.algorithm,
true,
this.keyUsages
);
}
// Derive key from password
async deriveKey(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
this.algorithm,
true,
this.keyUsages
);
}
// Encrypt data
async encrypt(data, key) {
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encoder.encode(JSON.stringify(data))
);
// Combine iv and encrypted data
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
// Convert to base64 for storage
return btoa(String.fromCharCode(...combined));
}
// Decrypt data
async decrypt(encryptedData, key) {
// Convert from base64
const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
// Extract iv and encrypted data
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encrypted
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
// Secure storage methods
async setSecure(key, value, password) {
try {
// Generate salt for this item
const salt = crypto.getRandomValues(new Uint8Array(16));
const cryptoKey = await this.deriveKey(password, salt);
const encrypted = await this.encrypt(value, cryptoKey);
// Store encrypted data with salt
const storageItem = {
data: encrypted,
salt: btoa(String.fromCharCode(...salt))
};
localStorage.setItem(key, JSON.stringify(storageItem));
return true;
} catch (error) {
console.error('Encryption failed:', error);
return false;
}
}
async getSecure(key, password) {
try {
const stored = localStorage.getItem(key);
if (!stored) return null;
const storageItem = JSON.parse(stored);
const salt = Uint8Array.from(atob(storageItem.salt), c => c.charCodeAt(0));
const cryptoKey = await this.deriveKey(password, salt);
return await this.decrypt(storageItem.data, cryptoKey);
} catch (error) {
console.error('Decryption failed:', error);
return null;
}
}
// Session-based secure storage
async setSecureSession(key, value) {
// Generate a session key
if (!this.sessionKey) {
this.sessionKey = await this.generateKey();
}
const encrypted = await this.encrypt(value, this.sessionKey);
sessionStorage.setItem(key, encrypted);
}
async getSecureSession(key) {
if (!this.sessionKey) return null;
const encrypted = sessionStorage.getItem(key);
if (!encrypted) return null;
return await this.decrypt(encrypted, this.sessionKey);
}
}
// Usage example
const secureStorage = new SecureStorage();
// Store sensitive data securely
async function storeSensitiveData() {
const sensitiveData = {
apiKey: 'secret-api-key',
userPreferences: { theme: 'dark', language: 'en' }
};
await secureStorage.setSecure('userData', sensitiveData, 'user-password');
// Retrieve data
const retrieved = await secureStorage.getSecure('userData', 'user-password');
console.log('Retrieved:', retrieved);
}
Storage Quota Manager
Manage storage quotas effectively:
// Storage Quota Manager
class StorageQuotaManager {
// Check storage quota
async checkQuota() {
const quota = {
available: false,
usage: 0,
quota: 0,
percent: 0,
breakdown: {}
};
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
quota.available = true;
quota.usage = estimate.usage;
quota.quota = estimate.quota;
quota.percent = (estimate.usage / estimate.quota * 100).toFixed(2);
// Try to get breakdown by storage type
if (estimate.usageDetails) {
quota.breakdown = estimate.usageDetails;
}
} catch (error) {
quota.error = error.message;
}
}
// Add manual calculations
quota.breakdown.localStorage = this.calculateLocalStorageSize();
quota.breakdown.sessionStorage = this.calculateSessionStorageSize();
quota.breakdown.cookies = document.cookie.length;
return quota;
}
// Calculate localStorage size
calculateLocalStorageSize() {
let size = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
size += localStorage[key].length + key.length;
}
}
return size;
}
// Calculate sessionStorage size
calculateSessionStorageSize() {
let size = 0;
for (let key in sessionStorage) {
if (sessionStorage.hasOwnProperty(key)) {
size += sessionStorage[key].length + key.length;
}
}
return size;
}
// Request persistent storage
async requestPersistence() {
if ('storage' in navigator && 'persist' in navigator.storage) {
try {
const isPersisted = await navigator.storage.persisted();
if (!isPersisted) {
const granted = await navigator.storage.persist();
return {
granted,
message: granted ?
'Persistent storage granted' :
'Persistent storage denied'
};
}
return {
granted: true,
message: 'Storage is already persistent'
};
} catch (error) {
return {
granted: false,
error: error.message
};
}
}
return {
granted: false,
error: 'Persistent storage API not supported'
};
}
// Clean up storage
async cleanup(options = {}) {
const report = {
cleared: [],
errors: [],
spaceSaved: 0
};
// Clear old items from localStorage
if (options.localStorage) {
const maxAge = options.maxAge || 30 * 24 * 60 * 60 * 1000; // 30 days
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
try {
const value = localStorage.getItem(key);
const data = JSON.parse(value);
if (data.timestamp && Date.now() - data.timestamp > maxAge) {
report.spaceSaved += key.length + value.length;
localStorage.removeItem(key);
report.cleared.push({ type: 'localStorage', key });
}
} catch (e) {
// Not JSON or no timestamp, skip
}
}
}
// Clear expired cookies
if (options.cookies) {
const cookies = document.cookie.split(';');
cookies.forEach(cookie => {
const [name] = cookie.trim().split('=');
// Set cookie with past expiration to delete
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
});
}
// Clear cache storage
if (options.cache && 'caches' in window) {
try {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
if (options.cacheWhitelist && !options.cacheWhitelist.includes(name)) {
await caches.delete(name);
report.cleared.push({ type: 'cache', name });
}
}
} catch (error) {
report.errors.push({ type: 'cache', error: error.message });
}
}
return report;
}
// Monitor storage events
monitorStorage(callback) {
// Listen for storage events (only fires for other tabs)
window.addEventListener('storage', (event) => {
callback({
type: 'storage',
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url,
storageArea: event.storageArea === localStorage ? 'localStorage' : 'sessionStorage'
});
});
// Override localStorage methods to monitor same-tab changes
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
const oldValue = localStorage.getItem(key);
originalSetItem.call(localStorage, key, value);
callback({
type: 'setItem',
key,
oldValue,
newValue: value,
storageArea: 'localStorage'
});
};
const originalRemoveItem = localStorage.removeItem;
localStorage.removeItem = function(key) {
const oldValue = localStorage.getItem(key);
originalRemoveItem.call(localStorage, key);
callback({
type: 'removeItem',
key,
oldValue,
newValue: null,
storageArea: 'localStorage'
});
};
return {
stop: () => {
window.removeEventListener('storage', callback);
localStorage.setItem = originalSetItem;
localStorage.removeItem = originalRemoveItem;
}
};
}
}
// Usage
const quotaManager = new StorageQuotaManager();
quotaManager.checkQuota().then(console.log);
Best Practices for Browser Storage
- Never Store Sensitive Data Unencrypted: Always encrypt PII and auth tokens
- Use the Right Storage Type: Session > Local > Cookies for sensitivity
- Implement Expiration: Add timestamps and clean up old data
- Handle Quota Errors: Always try-catch storage operations
- Validate Storage Data: Never trust data from storage without validation
- Use HTTP-Only Cookies: For authentication tokens
- Implement CSP: Prevent XSS attacks that could access storage
- Monitor Storage Usage: Track quota to prevent failures
- Clear on Logout: Remove all sensitive data on user logout
- Test Cross-Browser: Storage limits vary significantly
Common Storage Security Issues
- XSS Access: Any script can read localStorage/sessionStorage
- No Encryption: Data stored in plain text by default
- Quota Exceeded: Failing to handle storage limits
- Stale Data: Not implementing expiration/cleanup
- Cross-Tab Sync Issues: Storage events don't fire in same tab
- Third-Party Access: Embedded content accessing storage
- Network Interception: Unencrypted data in cookies
- Browser Extensions: Can access all storage
- Physical Access: Storage persists on disk
- Cross-Origin Leaks: Improper origin validation
Remember: Browser storage is convenient but not secure by default. Always assume storage can be accessed by malicious scripts and plan accordingly. Encrypt sensitive data, validate everything, and implement proper cleanup strategies.