DOM-Based Cross-Site Scripting
Summary
DOM XSS reports cluster around dangerous-sink usage in client-side JavaScript:
document.location/location.search/location.hashparsed and concatenated into innerHTML / document.write / evalpostMessagehandlers usingevent.datawithout 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, orJSON.parse(history.state). Bypasses include CSP-evasion via JSONP whitelisted endpoints, AngularJS template injection, and usingsrcdocof an iframe. Source maps and full JS bundles often expose the sink directly, helping triage.
Top Affected Components / Targets
SPA routes that use location.search or location.hash for statepostMessage-based widgets and embedsjQuery-heavy legacy front-endsAngularJS 1.x sites with sandbox bypass payloadsDocumentation/blog systems rendering user-controlled markdown client-sideAnalytics / experiment platforms that consume URL paramsThird-party widgets (chat, support, embed) with inadequate origin checksBrowser extensions / mobile-app webviews with file:// or content:// origins
Common Attack Vectors
Set location.hash to payload and watch DOM mutateSend postMessage from attacker iframe with payloadInject into URL parameter that the client JS parsesPlace payload in ?utm_* / ?ref= / ?callback= sort of common paramsSubmit AngularJS template payload {{constructor.constructor('alert(1)')()}}Use jsonp callback parameter that the page injectsClick crafted link where path segment becomes innerHTMLTrigger a deep-link with javascript: scheme inside SPA router
Common Payloads
#<svg/onload=alert(1)>?q=#'onload='alert(document.domain)?callback=alertjavascript: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.
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.
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.
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.
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.
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.