scanrub
DOM-Based Cross-Site Scripting

DOM-Based Cross-Site Scripting

4 min read 120 reports analyzed ScanRub Research
Share
Live Playground · Powered by this research
XSS Payload Preview
Raw (unencoded)
#<svg/onload=alert(1)>
Dangerous if reflected without encoding
HTML entity encoded
#&lt;svg/onload=alert(1)&gt;
Safe when output to HTML body
Attribute encoded
#&#60;svg/onload=alert(1)&#62;
Safe when output to HTML attribute
JavaScript string escaped
#\x3csvg\x2fonload=alert(1)\x3e
Safe inside a JS string context
Rule of thumb: encode output based on where it's going. HTML body ≠ HTML attribute ≠ JavaScript string ≠ CSS ≠ URL. Wrong context = XSS.
Payloads from this research (10 total)
#<svg/onload=alert(1)>
?q=#'onload='alert(document.domain)
?callback=alert
javascript:alert(1) (in routes that use location.replace)
These payloads were synthesized from real HackerOne disclosures. Click any payload to copy it, then paste it into the tester above to see how our detection classifies it.

Summary

DOM XSS reports cluster around dangerous-sink usage in client-side JavaScript:

  • document.location / location.search / location.hash parsed and concatenated into innerHTML / document.write / eval
  • postMessage handlers using event.data without origin check writing to innerHTML
  • jQuery $() / $.html() / .attr('href', ...) / .attr('src', ...) with user-controlled string
  • marked() / template engines rendering user-controlled markdown into innerHTML
  • URL-source-to-sink chains where the source is URLSearchParams, decodeURIComponent, or JSON.parse(history.state). Bypasses include CSP-evasion via JSONP whitelisted endpoints, AngularJS template injection, and using srcdoc of an iframe. Source maps and full JS bundles often expose the sink directly, helping triage.
◈ flow diagram
URL Fragment…Client-Side …JavaScript S…DOM RenderedScript Execu…
◈ chart
Critical
10
High
30
Medium
50
Low
30

Top Affected Components / Targets

  • SPA routes that use location.search or location.hash for state
  • postMessage-based widgets and embeds
  • jQuery-heavy legacy front-ends
  • AngularJS 1.x sites with sandbox bypass payloads
  • Documentation/blog systems rendering user-controlled markdown client-side
  • Analytics / experiment platforms that consume URL params
  • Third-party widgets (chat, support, embed) with inadequate origin checks
  • Browser extensions / mobile-app webviews with file:// or content:// origins

Common Attack Vectors

  • Set location.hash to payload and watch DOM mutate
  • Send postMessage from attacker iframe with payload
  • Inject into URL parameter that the client JS parses
  • Place payload in ?utm_* / ?ref= / ?callback= sort of common params
  • Submit AngularJS template payload {{constructor.constructor('alert(1)')()}}
  • Use jsonp callback parameter that the page injects
  • Click crafted link where path segment becomes innerHTML
  • Trigger a deep-link with javascript: scheme inside SPA router

Common Payloads

  • #<svg/onload=alert(1)>
  • ?q=#'onload='alert(document.domain)
  • ?callback=alert
  • javascript:alert(1) (in routes that use location.replace)
  • {{constructor.constructor('alert(1)')()}} (Angular)
  • ?ref=<img src=x onerror=alert(1)>
  • postMessage payload: {"notes":"<svg/onload=alert(1)>"}
  • #data:text/html,<script>alert(1)</script>
  • ?#'onload='alert(1)
  • <iframe srcdoc="<svg/onload=alert(1)>">

Detection Strategy

DOM XSS is genuinely hard to detect from outside the application: black-box probing of innerHTML-style sinks isn't reliable without actually rendering the page in a browser. A practical approach layers a few techniques. First, static analysis of JS bundles can flag dangerous sink calls (innerHTML, outerHTML, document.write, eval, Function, location.replace, srcdoc, dangerouslySetInnerHTML, jQuery's .html and .attr, and message listeners with no origin check), which produces an informational signal scoped to the file even without proof of exploitability. Second, where source maps are available, tracing user-controlled sources (URL parameters, postMessage, storage) into those flagged sinks raises confidence considerably when the chain between source and sink is short. Third, and most reliable, is a headless-browser probe: navigate to each route with a marker token placed in the URL's search string or hash, then inspect the rendered DOM for that token appearing unsanitized in a dangerous attribute, with the same browser session used to deliver a similarly poisoned postMessage. Fourth, when a known client-side templating framework is detected, a targeted template-injection payload can confirm sandbox-bypass bugs directly. Actual code execution observed in a real browser session is a high-confidence finding; a static sink match with no observed flow is informational only.

Tip: Testing tools that run these checks in parallel across every discovered endpoint can cut the time required substantially compared to fully manual testing, as long as they confirm findings with more than one signal to keep the false-positive rate down.

False-Positive Notes

  • Static sink-presence isn't a vulnerability by itself - most uses of innerHTML are with safe inputs.
  • The high-confidence path is browser-driven: poison the source, navigate, observe sink mutation.
  • Sourcemap-based tracing is high signal but only when sourcemaps exist (often only in dev builds shipped accidentally - also a finding).
  • PostMessage probing should be conservative - many legitimate widgets accept any origin and would erroneously flag.

How to Test

Manual Testing Methodology

Here is a systematic approach to identifying DOM-Based Cross-Site Scripting vulnerabilities in a target application.

Step 1: Reconnaissance and Surface Mapping

Before testing, map all input vectors that could be affected. Identify parameters, headers, cookies, and request bodies that interact with the vulnerable component. A proxy such as Burp Suite or OWASP ZAP, paired with normal browsing of the target, is usually enough to build this list.

Step 2: Baseline Request

Send a legitimate request and record the normal response: status code, content length, response time, and any identifying tokens. This baseline matters because it's what you'll compare later responses against once payloads are involved.

Step 3: Payload Injection

Inject test payloads into each identified input vector one at a time. Start with benign detection payloads before escalating to anything that could actually trigger the vulnerability. For DOM-Based Cross-Site Scripting specifically, trace each client-side source (location.hash, location.search, document.referrer, postMessage payloads, stored data rendered client-side) forward through the bundled JavaScript to see which ones reach a sink, then craft a payload for that specific source — a hash-based payload never reaches the server at all, so it has to be tested by loading the URL in a browser, not by inspecting HTTP responses.

Step 4: Response Analysis

Compare the response against your baseline, looking specifically for execution in the browser itself (an alert firing, a network callback landing) rather than anything visible in the HTTP response — code review of the bundle or a headless-browser test harness catches this class far more reliably than response diffing.

Step 5: Confirmation

Once a potential vulnerability is detected, confirm it with at least a few independent test cases to rule out coincidence. Document the exact request and response as proof. For DOM-Based Cross-Site Scripting, a confirmed finding typically means showing that attacker-controlled input changes the application's behavior in a way that matters for security, not just that a payload was reflected somewhere harmless.

Real-World Impact

Real-World Impact

DOM-based XSS executes entirely client-side: attacker-controlled data flows from a source such as location.hash, document.referrer, or a URL parameter into a dangerous sink such as innerHTML, document.write, or eval without ever passing back through the server. That means it's invisible to server-side logs and to any WAF that only inspects requests, not client-side execution.

The practical impact mirrors server-rendered XSS — session hijacking, credential phishing, and full account takeover once the attacker has script execution in the victim's authenticated context — but the discovery and remediation path is different, since the vulnerable code lives in a JavaScript bundle rather than a server template.

Single-page applications are especially exposed here: heavy client-side routing and state management means far more code paths read from location/document and write into the DOM than a traditional server-rendered page ever had.

Prevention & Remediation

Prevention and Secure Coding

Preventing DOM-Based Cross-Site Scripting takes a defense-in-depth approach — no single control below is sufficient alone, but together they close off both the primary path and the most common bypasses.

Avoid dangerous sinks. Prefer textContent and createElement/setAttribute over innerHTML, outerHTML, and document.write — the safe APIs simply don't interpret their input as markup.

Sanitize before any HTML insertion. Where inserting real HTML is unavoidable, run it through a maintained sanitizer such as DOMPurify immediately before assignment, not at some earlier point the value might be transformed after.

Never `eval` or `Function()` on external data. Treat eval, new Function(), setTimeout/setInterval with a string argument, and dynamic import() of a URL-derived path as equivalent to innerHTML — all of them execute attacker-controlled strings as code.

Trusted Types. In browsers that support it, a Trusted Types CSP policy turns dangerous-sink assignment into a build-time and runtime error unless the value was produced by an approved sanitizer, which closes off the whole bug class rather than one instance of it.

Audit third-party scripts too. A vulnerable sink inside an embedded widget or ad script executes with the same DOM access as first-party code — third-party JavaScript needs the same source-to-sink review.

Source reports

A sample of the disclosed HackerOne reports this playbook was synthesized from.

Frequently Asked Questions

What is DOM-Based Cross-Site Scripting?
DOM XSS reports cluster around dangerous-sink usage in client-side JavaScript: (1) `document.location` / `location.search` / `location.hash` parsed and concatenated into innerHTML / document.write / eval; (2) `postMessage` handlers using `event.data` without origin check writing to innerHTML; (3) jQuery `$()` / `$.html()` / `.attr('href', ...)` / `.attr('src', ...)` with user-controlled string; (4) `marked()` / template engines rendering user-controlled markdown into innerHTML; (5) URL-source-to-sink chains where the source is `URLSearchParams`, `decodeURIComponent`, or `JSON.parse(history.state)`.
How common is DOM-Based Cross-Site Scripting in bug bounty reports?
Scanrub's research corpus for this playbook is built from 120 disclosed HackerOne reports in this category, synthesized for detection and prevention guidance rather than reproduced verbatim.
How do I test for DOM-Based Cross-Site Scripting?
Map every input vector, record a baseline response, then inject targeted test payloads one field at a time and compare the response for timing, length, error, or reflection differences from that baseline. The "How to Test" section above walks through the full methodology for this specific vulnerability class.
What is the single most effective fix for DOM-Based Cross-Site Scripting?
Eliminate dangerous DOM sinks (`innerHTML`, `document.write`, `eval`) in favor of `textContent`/safe DOM APIs, and sanitize with DOMPurify immediately before any point where real HTML must be inserted.
Weekly security research

New vulnerability playbooks, tool updates, and bug bounty insights - delivered to your inbox. No spam.

Unsubscribe anytime. We respect your inbox.
Press ⌘K to search×