Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 271/271 feeds/APIs · 464/464 docs · 8/9 advisories · checked 04:30 UTC Customize Public modeDiscuss ChangeIntel on Discord

DOMPurify before 3.4.7 Hook Mutation Pollution via allowedTags

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-65902

Published

DOMPurify before 3.4.7 (affected versions <= 3.4.5) passes direct references to the module-level DEFAULT_ALLOWED_TAGS and DEFAULT_ALLOWED_ATTR sets to the uponSanitizeElement and uponSanitizeAttribute hooks via data.allowedTags / data.allowedAttributes when sanitize is called without an explicit cfg.ALLOWED_TAGS / cfg.ALLOWED_ATTR array. A hook that mutates these fields permanently widens the default allow-lists for the lifetime of the DOMPurify instance, so all subsequent default-config sanitize calls inherit the widened defaults and attacker payloads using the poisoned tag/attribute name survive sanitization. removeAllHooks(), clearConfig(), and passing a fresh cfg do not recover the state; only constructing a new DOMPurify instance does.

Source published
23 Jul 2026 13:16:20 UTC
Source modified
24 Jul 2026 21:35:25 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N

CVSS 3.1 · 6.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

nvd · CVE-2026-65902

Published

DOMPurify before 3.4.7 (affected versions <= 3.4.5) passes direct references to the module-level DEFAULT_ALLOWED_TAGS and DEFAULT_ALLOWED_ATTR sets to the uponSanitizeElement and uponSanitizeAttribute hooks via data.allowedTags / data.allowedAttributes when sanitize is called without an explicit cfg.ALLOWED_TAGS / cfg.ALLOWED_ATTR array. A hook that mutates these fields permanently widens the default allow-lists for the lifetime of the DOMPurify instance, so all subsequent default-config sanitize calls inherit the widened defaults and attacker payloads using the poisoned tag/attribute name survive sanitization. removeAllHooks(), clearConfig(), and passing a fresh cfg do not recover the state; only constructing a new DOMPurify instance does.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Analyzed

Weakness classifications: CWE-501

Original publisher date fields

published: 2026-07-23T14:18:05.453

lastModified: 2026-10-24T13:10:00.147

1 source CPE criteria · context only
  • cpe:2.3:a:cure53:dompurify:*:*:*:*:*:*:*:*

The original JSON retains the logical configuration operators, exclusions and version bounds.

Source published
23 Jul 2026 14:18:05 UTC
Source modified
24 Oct 2026 13:10:00 UTC
Observed here
25 Oct 2026 15:18:51 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · [email protected] · Secondary · 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

CVSS 3.1 · [email protected] · Primary · 6.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

osv · GHSA-76mc-f452-cxcm

Published

# Hook mutation of `data.allowedTags` / `data.allowedAttributes` permanently pollutes `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR` **CWE**: CWE-501 (Trust Boundary Violation — hook-scoped mutation leaks to global default sets) via CWE-693 (Protection Mechanism Failure — the default allow-list is silently widened for all subsequent sanitize calls) ## Summary The `data.allowedTags` and `data.allowedAttributes` fields passed to `uponSanitizeElement` and `uponSanitizeAttribute` hooks are **direct references** to the library's live `ALLOWED_TAGS` / `ALLOWED_ATTR` sets. For sanitize calls that don't supply an explicit `cfg.ALLOWED_TAGS` / `cfg.ALLOWED_ATTR` array, those live sets are themselves direct references to the module-level `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR` constants. A hook that mutates these fields — a natural-looking pattern for "allow `X` for this iteration" — permanently writes new entries into the default constants for the DOMPurify instance's lifetime. Every subsequent sanitize call that doesn't override the config inherits the widened defaults, so an attacker payload that uses the poisoned tag/attribute name survives sanitization. `removeAllHooks()`, `clearConfig()`, and even passing a fresh `cfg: {}` do not recover; only constructing a new DOMPurify instance does. The maintainer's existing defense at `src/purify.ts:696-700` explicitly clones `DEFAULT_ALLOWED_TAGS` before mutating it via `cfg.ADD_TAGS` (array form), demonstrating awareness of this exact class. The hook path remained uncovered. ## Affected - DOMPurify ≤ 3.4.5, including `main` at `7996f1dc78eb8b7922388aed75d94a9f8fad9a36` - Any application that installs a hook on `uponSanitizeElement` or `uponSanitizeAttribute` that writes to `data.allowedTags[...] = true` or `data.allowedAttributes[...] = true` and later sanitizes attacker-influenced content with default config (no explicit `cfg.ALLOWED_TAGS` / `cfg.ALLOWED_ATTR` array) ## Vulnerability details ### [A] — `data.allowedTags` is a reference to `ALLOWED_TAGS` `src/purify.ts:1206-1209`: ```ts _executeHooks(hooks.uponSanitizeElement, currentNode, { tagName, allowedTags: ALLOWED_TAGS, // [A] direct reference; hook mutation // mutates the very ALLOWED_TAGS the // library checks on the next element }); ``` `src/purify.ts:1494-1500` (the matching attribute hook): ```ts const hookEvent = { attrName: '', attrValue: '', keepAttr: true, allowedAttributes: ALLOWED_ATTR, // [A'] same pattern forceKeepAttr: undefined, }; ``` ### [B] — `ALLOWED_TAGS = DEFAULT_ALLOWED_TAGS` for default-cfg sanitize calls `src/purify.ts:527-531`: ```ts ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; // [B] reference assignment; ALLOWED_TAGS // IS the DEFAULT_ALLOWED_TAGS object ``` (The `ALLOWED_ATTR = DEFAULT_ALLOWED_ATTR` path at `:532-536` is symmetric.) ### The mismatch A hook author who writes `data.allowedTags['script'] = true` reasonably expects per-call scope — the API name is *"data"*, suggesting per-event payload. But [A] makes this a direct reference, and [B] makes that reference equal to the module-level default for the common default-cfg path. The hook's mutation therefore writes to a *constant* that every subsequent default-cfg sanitize call rebinds to. The maintainer already recognized this class for the `ADD_TAGS` array path — `src/purify.ts:696-700`: ```ts } else if (arrayIsArray(cfg.ADD_TAGS)) { if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS = clone(ALLOWED_TAGS); // explicitly clone DEFAULT before // mutating to avoid this pollution } addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); } ``` The same defensive clone is missing from the hook code paths. ## Proof of concept ```js // 1) fresh DOMPurify, default config — script is blocked DOMPurify.sanitize('<svg><script>alert(1)</script></svg>'); // → "<svg></svg>" // 2) install a hook that mutates data.allowedTags (natural-looking pattern) DOMPurify.addHook('uponSanitizeElement', (node, data) => { data.allowedTags['script'] = true; }); // 3) one sanitize call WITH the hook — script survives (expected during the hook) DOMPurify.sanitize('<svg><script>alert(1)</script></svg>'); // → "<svg><script>alert(1)</script></svg>" // 4) remove the hook DOMPurify.removeAllHooks(); DOMPurify.clearConfig(); // 5) sanitize attacker content with default config — POLLUTION PERSISTS DOMPurify.sanitize('<svg><script>alert(1)</script></svg>'); // → "<svg><script>alert(1)</script></svg>" ← script survived without any hook // 6) the only recovery: create a fresh DOMPurify instance const fresh = DOMPurify(window); fresh.sanitize('<svg><script>alert(1)</script></svg>'); // → "<svg></svg>" ← clean ``` Observed (Chromium 148.0.7778.96, DOMPurify HEAD `7996f1d`): | step | input | output | bypass? | |---|---|---|---| | 1 fresh baseline | `<svg><script>__</script></svg>` | `<svg></svg>` | no | | 1b fresh baseline | `<a onclick=__>x</a>` | `<a>x</a>` | no | | 2 with hook (script) | `<svg><script>__</script></svg>` | `<svg><script>__</script></svg>` | yes (expected) | | 2b with hook (onclick) | `<a onclick=__>x</a>` | `<a onclick="__">x</a>` | yes (expected) | | 3 after `removeAllHooks()` | same | `<svg><script>__</script></svg>` | **YES (pollution)** | | 3b after `removeAllHooks()` | same | `<a onclick="__">x</a>` | **YES (pollution)** | | 4 after `clearConfig()` | same | `<svg><script>__</script></svg>` | **YES** | | 4b after `clearConfig()` | same | `<a onclick="__">x</a>` | **YES** | | 5 explicit restrictive `cfg.ALLOWED_TAGS=['svg']` | same | `<svg></svg>` | no (cloned set) | | 6 back to no cfg | same | `<svg><script>__</script></svg>` | **YES** | | 6b back to no cfg | same | `<a onclick="__">x</a>` | **YES** | | 7 fresh `DOMPurify(window)` instance | same | `<svg></svg>` | no | | 7b fresh instance | `<a onclick=__>x</a>` | `<a>x</a>` | no | ## Impact ### Direct Any application using `DOMPurify` that has any registered hook with the pattern `data.allowedTags[...] = true` or `data.allowedAttributes[...] = true`. The hook need not be designed to be permissive — it might be intended to *temporarily* allow a custom tag for one specific element shape. After the hook has executed even once, every subsequent default-config sanitize call carries the widened defaults, including: - attacker content rendered via separate code paths (e.g., the same library serving a comments section and a profile bio, where the bio uses the hook and the comments use plain `DOMPurify.sanitize(text)`) - third-party libraries that call `DOMPurify.sanitize` on the same instance The bypass survives `DOMPurify.removeAllHooks()` and `DOMPurify.clearConfig()` — the obvious "reset" calls a dev would reach for. Detection requires reading the `DEFAULT_ALLOWED_TAGS` / `DEFAULT_ALLOWED_ATTR` sets directly, which are not part of the public API. ### Indirect / second-order - **Editor / preview libraries** that compose with DOMPurify — if any consumer registers a hook that mutates `data.allowedTags`, every other consumer's sanitize calls inherit the widening. - **Test suites** that exercise multiple sanitize configurations — once a test's hook pollutes the defaults, later tests that assume default behavior may pass with widened defaults and miss real regressions. - **Long-running servers** (SSR, edge functions) that reuse a single DOMPurify instance — pollution accumulates over the process lifetime. ### Why the existing maintainer defense for `ADD_TAGS` doesn't catch this `src/purify.ts:696-700` already documents awareness: ```ts } else if (arrayIsArray(cfg.ADD_TAGS)) { if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS = clone(ALLOWED_TAGS); } addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); } ``` The clone-before-mutate pattern is exactly what's needed at the hook callsites (`:1206-1209` and `:1494-1500`) but was not extended there. The new entries this report's bypass adds to the defaults survive the same way `ADD_TAGS` array entries would have survived before that fix landed. ## Suggested fix Three minimal-impact options, in order of preference: 1. **Hand the hook a defensive copy** (most surgical): ```ts _executeHooks(hooks.uponSanitizeElement, currentNode, { tagName, allowedTags: { ...ALLOWED_TAGS }, // shallow copy; mutations stay scoped }); ``` Doc note: "`data.allowedTags` is a snapshot; to widen the live set, use `cfg.ADD_TAGS` or set the value to true in the snapshot and check the snapshot from a subsequent attribute hook." Hooks that read it for inspection still work; hooks that intended cross-call mutation must be rewritten to use a proper config path (which is the correct API anyway). 2. **Clone-on-write inside the hook path**, mirroring the existing `ADD_TAGS` defense at `:696-700`: detect that `ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS` after the hook returns, and if so, replace it with a clone for subsequent processing. This preserves the live-mutation semantics for in-call effects while preventing cross-call leakage. 3. **Lazy-clone `ALLOWED_TAGS`/`ALLOWED_ATTR` from defaults on first mutation**: install a Proxy or accessor that triggers a clone before mutation. Largest surface area, but bulletproof. Option (1) is the cleanest API contract: hook event objects should be event-local, never references to library-internal state.

Source published
15 Jun 2026 19:59:09 UTC
Source modified
10 Sep 2026 03:50:48 UTC
Observed here
24 Oct 2026 13:17:17 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Publisher rating · MODERATE

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 26 Oct 04:30 UTC · 271 of 271 readable · 269 fully read · documentation 464/464 current