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 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:
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
}
});
CVSS
CVSS 3.1 Base Score: 7.1 (High)
AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N
- Attack Vector: Network (attacker submits HTML to sanitize)
- Attack Complexity: High (requires prior prototype pollution)
- Privileges Required: None
- User Interaction: Required (victim must view the sanitized content)
- Scope: Changed (XSS can access other origins via cookies, session tokens)
- Confidentiality: High (full session hijack via event handlers)
- Integrity: Low (can modify page content)
- Availability: None
Note: The standalone CVSS without chaining context would be lower (~3.1, similar to GHSA-cj63-jhhr-wcxv), since prototype pollution is a prerequisite. However, PP gadgets are extremely prevalent in JavaScript ecosystems (lodash, jQuery, qs, deep-extend, merge-deep, etc.), making this practically exploitable in many real-world applications.
CWE
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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
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 (noCUSTOM_ELEMENT_HANDLINGoption), a prior prototype pollution gadget can inject permissivetagNameCheckandattributeNameCheckregex values intoObject.prototype, causing DOMPurify to allow arbitrary custom elements with arbitrary attributes — including event handlers — through sanitization.Affected Versions
Object.create(null)for initialization, no|| {}reassignment)|| {}reassignment was introduced in the 3.0.0→3.0.1 refactorRoot Cause
In
purify.jsat line 590, during config parsing:When no
CUSTOM_ELEMENT_HANDLINGis specified in the config (the default usage pattern),cfg.CUSTOM_ELEMENT_HANDLINGisundefined, and the fallback{}is used. This plain object inherits fromObject.prototype.Lines 591-598 then check
cfg.CUSTOM_ELEMENT_HANDLING(the original config property) — which isundefined— so the conditional blocks that would settagNameCheckandattributeNameCheckfrom the config are never entered.As a result,
CUSTOM_ELEMENT_HANDLING.tagNameCheckandCUSTOM_ELEMENT_HANDLING.attributeNameCheckresolve via the prototype chain. If an attacker has pollutedObject.prototype.tagNameCheckandObject.prototype.attributeNameCheckwith 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
DOMPurify.sanitize(userInput)call is affected.Proof of Concept
Tested configurations that are vulnerable:
DOMPurify.sanitize(input)DOMPurify.sanitize(input, {})DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: null })DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: {} })Suggested Fix
Change line 590 from:
To:
The
create(null)function (already used elsewhere in DOMPurify, e.g., inclone()) 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_HANDLINGin their config:CVSS
CVSS 3.1 Base Score: 7.1 (High)
AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:NNote: The standalone CVSS without chaining context would be lower (~3.1, similar to GHSA-cj63-jhhr-wcxv), since prototype pollution is a prerequisite. However, PP gadgets are extremely prevalent in JavaScript ecosystems (lodash, jQuery, qs, deep-extend, merge-deep, etc.), making this practically exploitable in many real-world applications.
CWE
Timeline
Credit
https://github.com/trace37labs