Description
DOM-based cross-site scripting (DOM XSS) is a variant of CWE-79 where the entire attack flow occurs client-side. Unlike reflected or stored XSS — where the server includes unsanitized content in its HTTP response — DOM XSS arises when client-side JavaScript reads data from an attacker-controlled source (a location in the DOM or browser API) and writes it to a dangerous sink (a DOM property or function that executes code). The server's response is benign; the vulnerability exists entirely in the browser-side code.
Common sources include document.URL, document.location.hash, document.location.search, document.referrer, window.name, and postMessage event data. Common sinks include innerHTML, outerHTML, document.write(), eval(), setTimeout() with string arguments, jQuery.html(), location.href, and src attributes on dynamically created script elements.
DOM XSS is particularly prevalent in single-page applications (SPAs) built with React, Angular, Vue, and Backbone, as well as in legacy jQuery-heavy codebases. Server-side scanners miss it entirely because the payload never touches the server. It falls under OWASP A03:2021 (Injection) and requires dedicated client-side testing techniques.
How It Works
A vulnerable SPA renders a search term from the URL fragment into the page:
// Vulnerable JavaScript
document.getElementById('results-heading').innerHTML =
'Search results for: ' + decodeURIComponent(location.hash.slice(1));
The URL fragment is never sent to the server. An attacker crafts:
https://target.com/search#<img src=x onerror=alert(document.cookie)>
When the victim visits this URL, the JavaScript reads the fragment, decodes it, and writes it to innerHTML — executing the onerror handler in the victim's browser.
document.write sink:
// Fragment: #</title><script>alert(1)</script>
document.write('<title>Search: ' + location.hash.slice(1) + '</title>');
eval sink via JSON parsing:
var config = eval('(' + location.search.replace('?config=','') + ')');
Payload: ?config=alert(document.cookie)//
postMessage source — DOM XSS via cross-origin messaging without origin validation:
window.addEventListener('message', function(e) {
document.getElementById('output').innerHTML = e.data;
});
Any origin can send a postMessage with an XSS payload. This is common in embedded widgets and OAuth popup flows.
Testing tools: DOM Invader (built into Burp Suite's browser) automatically identifies sources and sinks by instrumenting browser APIs. DOMinator (commercial) and manual review with browser DevTools are also standard approaches.
Impact
- Session Hijacking — Stealing
document.cookievalues via JavaScript execution in the victim's browser context - Account Takeover — Performing authenticated API calls (password change, email update) using the victim's active session
- Credential Harvesting — Injecting fake login forms over the existing page to capture plaintext credentials
- Browser-Based Pivoting — Using the victim's browser as a proxy to reach internal applications via
fetch()to internal IP ranges - Persistent Backdoor — Storing a malicious service worker that intercepts all future requests from that origin
Detection
- Enable DOM Invader in Burp Suite's embedded browser — DOM Invader automatically instruments the page, identifies all sources and sinks, and tests injection paths with a canary value. It detects sinks missed by static analysis.
- Manually audit JavaScript source files — grep for dangerous sinks:
innerHTML,outerHTML,document.write,eval,setTimeoutwith string args,setIntervalwith string args,location.href =,location =,src =. - Test all URL-readable sources — inject a canary string into
location.hash,location.search,document.referrer(via Referer header), andwindow.nameand search for it in all sink calls using browser DevTools breakpoints. - Test
postMessagehandlers — use the browser console to send arbitrarypostMessageevents and observe which handlers process them and whether they validateevent.origin. - Review JavaScript frameworks for unsafe patterns — look for
dangerouslySetInnerHTMLin React,[innerHTML]binding in Angular without DomSanitizer, andv-htmlin Vue. - Use static analysis tools — CodeQL has DOM XSS query packs for JavaScript/TypeScript. Run
codeql database analyzewith thejavascript/dom-based-xssquery to identify source-to-sink paths statically.
Remediation
Prefer safe DOM APIs over dangerous sinks. Use textContent or innerText instead of innerHTML when inserting plain text:
// Vulnerable
element.innerHTML = userControlledValue;
// Safe
element.textContent = userControlledValue;
Sanitize HTML before inserting into the DOM. If rich HTML is required, use DOMPurify:
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userControlledValue);
Validate postMessage origin. Always check event.origin before processing message data:
window.addEventListener('message', function(e) {
if (e.origin !== 'https://trusted-origin.com') return;
// safe to process e.data
});
Use framework-native escaping. In React, pass data as props (auto-escaped). In Angular, use the DomSanitizer for HTML binding. Avoid bypassing the framework's built-in escaping mechanisms.
Deploy Trusted Types. Trusted Types is a browser mechanism that restricts dangerous sink assignments to those processed by a registered policy — enforcing sanitization at the platform level.
