Update dependency dompurify to v3.4.11 [SECURITY]#299
Merged
Conversation
cb2a69e to
3e911e7
Compare
3e911e7 to
763498a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
3.4.8→3.4.11DOMPurify: Trusted Types policy survives
clearConfig()and can poison laterRETURN_TRUSTED_TYPEoutputGHSA-vxr8-fq34-vvx9
More information
Details
Impact
A DOMPurify instance that is reused across trust boundaries can stay bound to a previously supplied
TRUSTED_TYPES_POLICYeven afterclearConfig()is called. A later caller that requestsRETURN_TRUSTED_TYPEreceives aTrustedHTMLobject created by the old policy, not by a clean default configuration.If the old policy is unsafe or controlled by a less-trusted integration, this turns a later "default" sanitize call into script execution at a Trusted Types sink.
TRUSTED_TYPES_POLICY: nullon the later call also does not clear the retained policy.dompurify-trusted-types-policy-survives-clearconfig-poc.js
Affected version
Tested against DOMPurify
3.4.8, repository commit825e617753ac1169306a542d3174a77f717a0cf6.Root cause
_parseConfig()overwritestrustedTypesPolicywhencfg.TRUSTED_TYPES_POLICYis truthy, but the default/null path only initializes the internal policy whentrustedTypesPolicy === undefined. Once a custom policy has been set, later default config parsing leaves it in place.Relevant code:
src/purify.ts:786-812accepts and storescfg.TRUSTED_TYPES_POLICY.src/purify.ts:813-832does not reset an existing policy when config has no policy or hasTRUSTED_TYPES_POLICY: null.src/purify.ts:2123-2125signs the final serialized HTML with the retained policy whenRETURN_TRUSTED_TYPEis true.src/purify.ts:2133-2136clearConfig()only clearsCONFIGandSET_CONFIG; it does not resettrustedTypesPolicyoremptyHTML.Local PoC
Run from the DOMPurify checkout, or set
DOMPURIFY_REPO:Observed output:
{ "result": { "baseline": "<b>baseline</b>", "duringPolicy": "<img src=x onerror=alert(\"TT_POLICY_SURVIVED_CLEARCONFIG\")>", "afterClearString": "<img src=\"x\">", "afterClearTrustedType": "[object TrustedHTML]", "afterClearTrusted": "<img src=x onerror=alert(\"TT_POLICY_SURVIVED_CLEARCONFIG\")>", "afterNullTrusted": "<img src=x onerror=alert(\"TT_POLICY_SURVIVED_CLEARCONFIG\")>", "mountedHTML": "<img src=\"x\" onerror=\"alert("TT_POLICY_SURVIVED_CLEARCONFIG")\">" }, "dialogs": [ "TT_POLICY_SURVIVED_CLEARCONFIG" ] }The important part is the split behavior after cleanup:
purify.clearConfig(); purify.sanitize(...);returns a normal sanitized string (<img src="x">), because the later call is not asking for a Trusted Type.purify.clearConfig(); purify.sanitize(..., { RETURN_TRUSTED_TYPE: true });still uses the old policy and returns attacker-controlledTrustedHTML.{ TRUSTED_TYPES_POLICY: null, RETURN_TRUSTED_TYPE: true }also still returns attacker-controlledTrustedHTML.Preconditions
This is a shared-instance state contamination issue. It matters when one DOMPurify instance is reused by multiple integrations, plugins, request handlers, or components with different trust levels, and a cleanup step relies on
clearConfig()to restore safe defaults.This is not a default string-input bypass. An attacker must be able to influence a prior
TRUSTED_TYPES_POLICYon the reused instance, or a less-trusted integration must have installed an unsafe policy.Severity
impact is XSS at a Trusted Types sink in applications that reuse a DOMPurify instance across trust boundaries. Attack complexity is high because exploitation depends on prior policy injection or a less-trusted integration and a later
RETURN_TRUSTED_TYPEsink.Suggested fix
Make
clearConfig()reset Trusted Types state as part of restoring defaults, or have_parseConfig()explicitly cleartrustedTypesPolicyandemptyHTMLwhenTRUSTED_TYPES_POLICY: nullis supplied.Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
DOMPurify: Permanent
ALLOWED_ATTRpollution viasetConfig()bypassing the hook clone-guard (incomplete fix of the 3.4.7 hook-pollution patch)GHSA-cmwh-pvxp-8882
More information
Details
Summary
DOMPurify 3.4.7 shipped a security fix ("permanent hook pollution") that makes a registered
uponSanitizeAttributehook's mutation ofdata.allowedAttributesnon-persistent — so allowing an attribute for one element does not leak into latersanitize()calls. The fix clonesALLOWED_ATTRinside_parseConfig.That guard is silently bypassed whenever the application uses the persistent-config API
DOMPurify.setConfig().setConfig()sets the module flagSET_CONFIG = true, which causessanitize()to skip_parseConfigentirely — and the clone-guard lives inside_parseConfig. The hook is then handed the live, sharedALLOWED_ATTRobject; anydata.allowedAttributes[name] = trueit writes mutates that shared object permanently, for the lifetime of the DOMPurify instance, across every subsequent call, and across all elements.If an application uses
setConfig()together with anuponSanitizeAttributehook that conditionally allows a dangerous attribute (onerror,onclick,onmouseover,srcdoc,formaction, …) for "trusted" elements, then one trusted render permanently allows that attribute on untrusted, attacker-controlled content — yielding stored XSS in viewers' browsers. DOMPurify applies no separate/^on/event-handler blocklist: attribute stripping is governed entirely by the allowlist, so a polluted allowlist is the only gate, and survival in the output is final.Affected configuration (preconditions)
The vulnerability is triggered when an application does both:
DOMPurify.setConfig(...)once (the recommended pattern for a fixed, persistent policy), anduponSanitizeAttributehook that writesdata.allowedAttributes[name] = trueto conditionally allow an attribute (e.g. only for elements bearing a trust marker).This hook pattern is demonstrated in DOMPurify's own test suite, and the per-call variant of exactly this leak is what 3.4.7 was released to fix.
Root cause (source:
src/purify.ts, v3.4.10)The 3.4.7 clone-guard — only inside
_parseConfig:sanitize()skips_parseConfigon the persistent-config path:setConfig()sets the flag that disables the guard:The hook is handed the live allowlist binding, and there is no secondary event-handler defense:
Net: after
setConfig(), the clone-guard never runs, so the hook'sallowedAttributesmutation is a permanent write to the instance's sharedALLOWED_ATTR.Proof of Concept
Environment:
npm i dompurify@3.4.10 jsdom(Node; identical mechanism toisomorphic-dompurify, and to a browser instance).PoC 1 — the leak (trusted render permanently allows
onerroron attacker content)PoC 2 — it is a DOMPurify state-leak, not "the app allowed
on*" (attribute-agnostic)PoC 3 — control: WITHOUT
setConfig()the 3.4.7 guard holdsPersistence (observed)
removeAllHooks()— removing the hook does not clean the polluted allowlist.onmouseoversurvives on<a>and<div>, not only the originally-blessed<img>.clearConfig()does restore a clean state (this is the bound of the impact).Impact
Stored XSS. In a long-lived (e.g. server-side /
isomorphic-dompurify) DOMPurify instance, a single trusted render flips a shared allowlist bit; every subsequent untrusted submission then inherits a live event-handler attribute and executes script in viewers' browsers. Because DOMPurify enforces no/^on/blocklist, a survivingon*attribute is final — no secondary control prevents execution.onerroron a broken-src<img>fires with no user interaction (browser-confirmed; see Validation).Per-call
FORBID_ATTRdoes not mitigate. A defensivesanitize(input, { FORBID_ATTR: ['onerror'] })is also ignored oncesetConfig()has been called: the per-call config is parsed by_parseConfig, whichsanitize()skips entirely underSET_CONFIG. So an application cannot blunt the leak with a per-call denylist — the poisonedALLOWED_ATTRis the sole gate.Realistic attack scenario
A platform mixes admin-authored interactive widgets with user-generated content through one sanitizer instance:
setConfig({ ALLOWED_TAGS: [...], ALLOWED_ATTR: [...] }).uponSanitizeAttributehook that enables an event handler only for admin-vetted elements markeddata-trusted="1", intending safe rich interactivity — a pattern the 3.4.7 fix was specifically meant to make safe.<img src=x onerror=...>passes sanitization and executes for all viewers.Remediation
Extend the existing clone-guard to the persistent-config (
SET_CONFIG) fast-path: whensanitize()skips_parseConfigbut anuponSanitizeAttributehook is registered, clone the allowlists before the walk so hook mutations cannot persist — the exact analogue of the guard already present in_parseConfig.(Equivalently: in the hook-event builder at line ~2088, hand the hook a shallow clone of
ALLOWED_ATTR/ALLOWED_TAGSwheneverSET_CONFIGis true, mirroring the 3.4.7 intent.)A regression test should reproduce PoC 1 and assert the attacker call returns
<img src="x">. Note the existing 3.4.7 regression test ("unguarded attribute hook does not poison subsequent default-config calls") never exercisessetConfig()— adding asetConfigvariant closes the gap.Application-side mitigation until patched: prefer
data.keepAttr = true(per-element, non-persistent) overdata.allowedAttributes[name] = trueinside hooks; or callDOMPurify.clearConfig()between trust domains; or use separate DOMPurify instances for trusted vs. untrusted content.Limitations
setConfig()and a hook writingdata.allowedAttributes[...]). Not a default-config bypass.clearConfig(), which restores a clean state. The earlier-considered "survivesclearConfig()" claim did not reproduce and is withdrawn.data.keepAttr=true, notallowedAttributes[]." However, the 3.4.7 security fix exists precisely to defend theallowedAttributes[]hook pattern in the per-call path; leaving thesetConfigpath unguarded is an incomplete fix of an acknowledged security issue.Validation
dompurify@3.4.10dist/purify.cjs.js(md5ab0e7b1cde1cbcace0f62b6aac284143) and browserdist/purify.min.js(md5b0985f80fa48e6e7b263f8f6a64b779e) are byte-identical to a freshlynpm pack-ed release — the repro is on the real shipped code. Mechanism identical on 3.4.0, 3.4.9 and 3.4.10.DOMPurify.isValidAttribute('img','onerror','x')flipsfalse → trueafter a single trusted render undersetConfig(), proving the shared attribute gate is poisoned. Leak survivesremoveAllHooks(), is cross-element, persists for the instance lifetime, and is reset only byclearConfig().innerHTMLexecutes the survivingonerror(sentinelwindow.__fired = ["ATTACKER-onerror"];onerrorDOM property is afunction), with no user interaction. The no-setConfigA/B control does not fire — execution is attributable to thesetConfigleak, not a harness artifact.Appendix A — Node PoC (complete, runnable)
Expected output:
Appendix B — Browser PoC (complete; confirms execution)
Observed:
handlers fired: ["alert:XSS:<domain>"]→ RESULT: XSS EXECUTED (no user interaction). The same harness without thesetConfig()line stripsonerrorand does not fire.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
cure53/DOMPurify (dompurify)
v3.4.11: DOMPurify 3.4.11Compare Source
setConfig, thanks @trace37labsnpm auditosv-scannersuppression list as no vulnerable dependencies are left for nowv3.4.10: DOMPurify 3.4.10Compare Source
types.tsSAFE_FOR_TEMPLATESscrubbing into single shared pathtextContentbeforeinnerHTMLnpm run bench) with a--comparemodedemos/folder so every demo runs again, and added a SVG-via-<img>demotest:happydomscripts in the READMEv3.4.9: DOMPurify 3.4.9Compare Source
IN_PLACEsanitization, thanks @mozfreddybIN_PLACEand Trusted Types related usageConfiguration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.