GHSA-V9JR-RG53-9PGP

Vulnerability from github – Published: 2026-04-22 17:31 – Updated: 2026-04-22 17:31
VLAI?
Summary
DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback
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 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
  }
});

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

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "dompurify"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.1"
            },
            {
              "fixed": "3.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-41238"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1321",
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-22T17:31:32Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nDOMPurify 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 \u2014 including event handlers \u2014 through sanitization.\n\n## Affected Versions\n\n- **3.0.1 through 3.3.3** (current latest) \u2014 all affected\n- **3.0.0 and all 2.x versions** \u2014 NOT affected (used `Object.create(null)` for initialization, no `|| {}` reassignment)\n- The vulnerable `|| {}` reassignment was introduced in the 3.0.0\u21923.0.1 refactor\n- This is **distinct** from GHSA-cj63-jhhr-wcxv (USE_PROFILES Array.prototype pollution, fixed in 3.3.2)\n- This is **distinct** from CVE-2024-45801 / GHSA-mmhx-hmjr-r674 (__depth prototype pollution, fixed in 3.1.3)\n\n## Root Cause\n\nIn `purify.js` at line 590, during config parsing:\n\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n```\n\nWhen 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`.\n\nLines 591-598 then check `cfg.CUSTOM_ELEMENT_HANDLING` (the original config property) \u2014 which is `undefined` \u2014 so the conditional blocks that would set `tagNameCheck` and `attributeNameCheck` from the config are never entered.\n\nAs 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\u0027s custom element validation at lines 973-977 and attribute validation, causing all custom elements and all attributes to be allowed.\n\n## Impact\n\n- **Attack type:** XSS bypass via prototype pollution chain\n- **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)\n- **Config required:** Default. No special DOMPurify configuration needed. The standard `DOMPurify.sanitize(userInput)` call is affected.\n- **Payload:** Any HTML custom element (name containing a hyphen) with event handler attributes survives sanitization\n\n## Proof of Concept\n\n```javascript\n// Step 1: Attacker exploits a prototype pollution gadget elsewhere in the application\nObject.prototype.tagNameCheck = /.*/;\nObject.prototype.attributeNameCheck = /.*/;\n\n// Step 2: Application sanitizes user input with DEFAULT config\nconst clean = DOMPurify.sanitize(\u0027\u003cx-x onfocus=alert(document.cookie) tabindex=0 autofocus\u003e\u0027);\n\n// Step 3: \"Sanitized\" output still contains the event handler\nconsole.log(clean);\n// Output: \u003cx-x onfocus=\"alert(document.cookie)\" tabindex=\"0\" autofocus=\"\"\u003e\n\n// Step 4: When injected into DOM, XSS executes\ndocument.body.innerHTML = clean; // alert() fires\n```\n\n### Tested configurations that are vulnerable:\n\n| Call Pattern | Vulnerable? |\n|---|---|\n| `DOMPurify.sanitize(input)` | YES |\n| `DOMPurify.sanitize(input, {})` | YES |\n| `DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: null })` | YES |\n| `DOMPurify.sanitize(input, { CUSTOM_ELEMENT_HANDLING: {} })` | NO (explicit object triggers L591 path) |\n\n## Suggested Fix\n\nChange line 590 from:\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n```\n\nTo:\n```javascript\nCUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);\n```\n\nThe `create(null)` function (already used elsewhere in DOMPurify, e.g., in `clone()`) creates an object with no prototype, preventing prototype chain inheritance.\n\n### Alternative application-level mitigation:\n\nApplications can protect themselves by always providing an explicit `CUSTOM_ELEMENT_HANDLING` in their config:\n\n```javascript\nDOMPurify.sanitize(input, {\n  CUSTOM_ELEMENT_HANDLING: {\n    tagNameCheck: null,\n    attributeNameCheck: null\n  }\n});\n```\n\n## Timeline\n\n- **2026-04-04:** Vulnerability discovered during automated DOMPurify fuzzing research (Fermat project)\n- **2026-04-04:** Confirmed in Chrome browser with DOMPurify 3.3.3\n- **2026-04-04:** Verified distinct from GHSA-cj63-jhhr-wcxv and CVE-2024-45801\n- **2026-04-04:** Advisory drafted, responsible disclosure initiated\n\n## Credit\n\nhttps://github.com/trace37labs",
  "id": "GHSA-v9jr-rg53-9pgp",
  "modified": "2026-04-22T17:31:32Z",
  "published": "2026-04-22T17:31:32Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/cure53/DOMPurify/security/advisories/GHSA-v9jr-rg53-9pgp"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/cure53/DOMPurify"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cure53/DOMPurify/releases/tag/3.4.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…