Most XSS defenses - output encoding on the server, a strict Content-Security-Policy - protect against payloads that travel through HTML rendered by the server. DOM-based XSS happens entirely in the browser, after the page has already loaded: JavaScript takes data from an untrusted source and writes it into a dangerous sink without ever touching the server again. A perfectly-encoded server response can still be exploited if the client-side code that runs afterward isn't careful.

The Source-to-Sink Model

Every DOM-based XSS vulnerability has the same shape: untrusted data flows from a source (something an attacker can influence) to a sink (a browser API that can execute code or change page behavior), without sanitization in between.

Common sources:

Common sinks:

innerHTML: The Most Common Sink in Practice

// Vulnerable: URL hash rendered directly as HTML
const name = location.hash.slice(1); // #
document.getElementById('greeting').innerHTML = `Hello, ${name}!`;
// The onerror handler executes the moment the (broken) image tag is parsed

Nothing about this requires the server to be involved at all - the payload lives entirely in the URL fragment (everything after #), which browsers never send to the server in the first place. A WAF or server-side input validation sees nothing, because there is nothing to see on that side.

// Fixed: textContent never interprets its argument as HTML
document.getElementById('greeting').textContent = `Hello, ${name}!`;

// If you genuinely need to render HTML (rich text, markdown output),
// sanitize it first with a purpose-built library
import DOMPurify from 'dompurify';
document.getElementById('content').innerHTML = DOMPurify.sanitize(untrustedHtml);

Framework Escape Hatches

Modern frameworks auto-escape by default, which is why DOM-based XSS in React or Vue code usually traces back to one specific, deliberately-named function that opts back out of that protection.

// React: dangerouslySetInnerHTML is named the way it is on purpose
function Comment({ userSuppliedHtml }) {
  return 
; // Any