Last updated: April 26, 2026
DOM-based XSS happens entirely in the browser — server never sees the attack. Modern frameworks (React, Vue, Angular) reduce reflected XSS but introduce DOM-XSS via dangerouslySetInnerHTML, v-html, [innerHTML]. This article covers DOM-XSS detection patterns and the framework-specific safe patterns.
The bug
// Vulnerable React
function Profile({ bio }) {
return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}
// User-controlled bio = "<img src=x onerror=alert(document.cookie)>"
// React renders the string as HTML; XSS
Vulnerable sinks across frameworks
| Framework | Sink |
|---|---|
| Vanilla JS | innerHTML, outerHTML, document.write, eval, setTimeout(string) |
| jQuery | $.html(), $() with HTML string, $.parseHTML |
| React | dangerouslySetInnerHTML |
| Vue | v-html |
| Angular | [innerHTML] + bypassSecurityTrust* |
| Svelte | {@html ...} |
Detection
# Static analysis with Semgrep
semgrep --config "p/javascript" --config "p/typescript" .
# Targeted regex for review
grep -rE "dangerouslySetInnerHTML|v-html|innerHTML|outerHTML|document\.write|eval\(|new Function\(" src/
# Burp Pro DOM Invader extension — interactive DOM-XSS exploration
# Right-click in Burp browser → DOM Invader
# Manual canary
# Inject <img src=x onerror=alert(1)> in every input that renders client-side
# Check if it executes in DevTools console
Source-to-sink analysis
DOM-XSS requires user-controlled source reaching dangerous sink. Sources include:
location.search,location.hash,location.pathnamedocument.referrer,document.cookiewindow.name,postMessagedata- localStorage / sessionStorage values
- API responses (if attacker can control them)
The fix
- Default to safe rendering — JSX
{value}in React,{{ value }}in Vue automatically escape - Sanitise before HTML rendering — DOMPurify is the standard.
DOMPurify.sanitize(userHtml) - CSP —
script-src 'self'blocks inline script execution; bypass becomes harder - Trusted Types (Chrome / Edge) —
require-trusted-types-for 'script'CSP directive forces all DOM sinks to receive Trusted Type objects, blocking strings entirely
Trusted Types example
// CSP header:
// Content-Security-Policy: require-trusted-types-for 'script'
// Now this throws:
element.innerHTML = "<img src=x onerror=alert(1)>";
// TypeError: This document requires 'TrustedHTML'
// Must use a policy:
const policy = trustedTypes.createPolicy('default', {
createHTML: (str) => DOMPurify.sanitize(str)
});
element.innerHTML = policy.createHTML(userInput);
// Sanitised before assignment
The takeaway
DOM-XSS is the modern XSS variant that traditional WAFs and server-side scanners miss. Source-to-sink analysis with Semgrep + Burp DOM Invader catches systematically. Trusted Types (where browser-supported) closes the bug class architecturally. Ship Trusted Types in your next CSP iteration.
Get a VAPT scoping call
Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.