dompurify 3.4.10 → 3.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/dompurify.svg)](https://www.npmjs.com/package/dompurify) [![License](https://img.shields.io/badge/license-MPL--2.0%20OR%20Apache--2.0-blue.svg)](https://github.com/cure53/DOMPurify/blob/main/LICENSE) [![Downloads](https://img.shields.io/npm/dm/dompurify.svg)](https://www.npmjs.com/package/dompurify) [![dependents](https://badgen.net/github/dependents-repo/cure53/dompurify?color=green&label=dependents)](https://github.com/cure53/DOMPurify/network/dependents) ![npm package minimized gzipped size (select exports)](https://img.shields.io/bundlejs/size/dompurify?color=%233C1&label=gzip) [![Cloudback](https://app.cloudback.it/badge/cure53/DOMPurify)](https://cloudback.it)
4
4
 
5
- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12162/badge)](https://www.bestpractices.dev/projects/12162) [![Build & Test](https://github.com/cure53/DOMPurify/actions/workflows/build-and-test.yml/badge.svg?branch=main)](https://github.com/cure53/DOMPurify/actions/workflows/build-and-test.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cure53/DOMPurify/badge)](https://scorecard.dev/viewer/?uri=github.com/cure53/DOMPurify) [![Socket Badge](https://badge.socket.dev/npm/package/dompurify/latest)](https://badge.socket.dev/npm/package/dompurify/latest) [![snyk.io package health](https://img.shields.io/badge/snyk.io%20package%20health-91%2F100-brightgreen)](https://security.snyk.io/package/npm/dompurify)
5
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12162/badge)](https://www.bestpractices.dev/projects/12162) [![Build & Test](https://github.com/cure53/DOMPurify/actions/workflows/build-and-test.yml/badge.svg?branch=main)](https://github.com/cure53/DOMPurify/actions/workflows/build-and-test.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/cure53/DOMPurify/badge)](https://scorecard.dev/viewer/?uri=github.com/cure53/DOMPurify) [![Socket Badge](https://badge.socket.dev/npm/package/dompurify/latest)](https://badge.socket.dev/npm/package/dompurify/latest) [![snyk.io package health](https://img.shields.io/badge/snyk.io%20package%20health-97%2F100-brightgreen)](https://security.snyk.io/package/npm/dompurify)
6
6
 
7
7
  DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
8
8
 
9
- It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.4.10**.
9
+ It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.4.12**.
10
10
 
11
11
  DOMPurify runs as JavaScript and works in all modern browsers (Safari (10+), Opera (15+), Edge, Firefox and Chrome - as well as almost anything else using Blink, Gecko or WebKit). It doesn't break on MSIE or other legacy browsers. It simply does nothing.
12
12
 
@@ -16,7 +16,25 @@ Our automated tests cover 9 browser/OS combinations on the current engines (Chro
16
16
 
17
17
  DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not. For more details please also read about our [Security Goals & Threat Model](https://github.com/cure53/DOMPurify/wiki/Security-Goals-&-Threat-Model). Please, read it. Like, really. And if you enjoy the gory details, the [Attack Classes & Bypass History](https://github.com/cure53/DOMPurify/wiki/Attack-Classes-&-Bypass-History) page catalogs the parser-mutation, namespace, clobbering, and template tricks DOMPurify defends against.
18
18
 
19
- The DOMPurify project inspired the creation of the [HTML Sanitizer API](https://wicg.github.io/sanitizer-api/#sanitizer), which is already shipping in [many browsers](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API#browser_compatibility).
19
+ The DOMPurify project inspired the creation of the [HTML Sanitizer API](https://wicg.github.io/sanitizer-api/#sanitizer), which is already shipping in [many browsers](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API#browser_compatibility). The same capability is now being standardized directly in the [WHATWG HTML specification](https://html.spec.whatwg.org/#html-sanitization).
20
+
21
+ ## Table of Contents
22
+
23
+ - [What does it do?](#what-does-it-do)
24
+ - [How do I use it?](#how-do-i-use-it)
25
+ - [Is there a demo?](#is-there-a-demo)
26
+ - [What if I find a _security_ bug?](#what-if-i-find-a-security-bug)
27
+ - [Some purification samples please?](#some-purification-samples-please)
28
+ - [What is supported?](#what-is-supported)
29
+ - [What about legacy browsers like Internet Explorer?](#what-about-legacy-browsers-like-internet-explorer)
30
+ - [What about DOMPurify and Trusted Types?](#what-about-dompurify-and-trusted-types)
31
+ - [Can I configure DOMPurify?](#can-i-configure-dompurify)
32
+ - [Persistent Configuration](#persistent-configuration)
33
+ - [Hooks](#hooks)
34
+ - [Removed Configuration](#removed-configuration)
35
+ - [Continuous Integration](#continuous-integration)
36
+ - [Security Mailing List](#security-mailing-list)
37
+ - [Who contributed?](#who-contributed)
20
38
 
21
39
  ## What does it do?
22
40
 
@@ -122,7 +140,7 @@ const clean = DOMPurify.sanitize('<s>hello</s>');
122
140
 
123
141
  Of course there is a demo! [Play with DOMPurify](https://cure53.de/purify)
124
142
 
125
- ## What if I find a _security_ bug?
143
+ ## What if I find a security bug?
126
144
 
127
145
  First of all, please immediately contact us via [email](mailto:mario@cure53.de) so we can work on a fix. [PGP key](https://keyserver.ubuntu.com/pks/lookup?op=vindex&search=0xC26C858090F70ADA)
128
146
 
@@ -145,7 +163,7 @@ These are just a taste. For the full taxonomy of attack classes these samples co
145
163
 
146
164
  ## What is supported?
147
165
 
148
- DOMPurify currently supports HTML5, SVG and MathML. DOMPurify per default allows CSS, HTML custom data attributes. DOMPurify also supports the Shadow DOM - and sanitizes DOM templates recursively. DOMPurify also allows you to sanitize HTML for being used with the jQuery `$()` and `elm.html()` API without any known problems.
166
+ DOMPurify currently supports HTML5, SVG and MathML. DOMPurify per default allows CSS, HTML custom data attributes. DOMPurify also supports the Shadow DOM - and sanitizes DOM templates recursively. DOMPurify also allows you to sanitize HTML for being used with the jQuery `$()` and `elm.html()` API without any known problems. For the exact set of elements and attributes permitted by default, see the [Default TAGs & ATTRIBUTEs allow-list & blocklist](https://github.com/cure53/DOMPurify/wiki/Default-TAGs-ATTRIBUTEs-allow-list-&-blocklist) wiki page.
149
167
 
150
168
  ## What about legacy browsers like Internet Explorer?
151
169
 
@@ -153,7 +171,7 @@ DOMPurify does nothing at all. It simply returns exactly the string that you fed
153
171
 
154
172
  ## What about DOMPurify and Trusted Types?
155
173
 
156
- In version 1.0.9, support for [Trusted Types API](https://github.com/w3c/webappsec-trusted-types) was added to DOMPurify.
174
+ In version 1.0.9, support for the [Trusted Types API](https://github.com/w3c/webappsec-trusted-types) ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API)) was added to DOMPurify.
157
175
  In version 2.0.0, a config flag was added to control DOMPurify's behavior regarding this.
158
176
 
159
177
  When `DOMPurify.sanitize` is used in an environment where the Trusted Types API is available and `RETURN_TRUSTED_TYPE` is set to `true`, it tries to return a `TrustedHTML` value instead of a string (the behavior for `RETURN_DOM` and `RETURN_DOM_FRAGMENT` config options does not change).
@@ -180,6 +198,8 @@ window.trustedTypes.createPolicy('my-organization', {
180
198
 
181
199
  Do **not** pass your own wrapping policy back to DOMPurify as its `TRUSTED_TYPES_POLICY` (for example via `DOMPurify.setConfig({ TRUSTED_TYPES_POLICY: myPolicy })`) when that policy's `createHTML` already calls `DOMPurify.sanitize`. That is circular by definition - sanitizing would call the policy, which sanitizes by calling DOMPurify again - and DOMPurify will throw a descriptive `TypeError` to prevent the infinite recursion. Your own policy should call DOMPurify; DOMPurify should not be configured to call your policy.
182
200
 
201
+ If you want this `default`-policy pattern applied across an entire page automatically - so that every HTML sink is sanitized, including legacy code, third-party widgets, and the thousands of `innerHTML` assignments you cannot easily find or rewrite - have a look at [DOMFortify](https://github.com/cure53/DOMFortify). It installs exactly such a Trusted Types `default` policy backed by DOMPurify and refuses script sinks (`eval`, `script.src`, ...) outright. It is a deliberately separate project: DOMPurify stays a focused sanitizer, and DOMFortify handles the document-wide enforcement layer that is intentionally out of DOMPurify's scope.
202
+
183
203
  ## Can I configure DOMPurify?
184
204
 
185
205
  Yes. The included default configuration values are pretty good already - but you can of course override them. Check out the [`/demos`](https://github.com/cure53/DOMPurify/tree/main/demos) folder to see a bunch of examples on how you can [customize DOMPurify](https://github.com/cure53/DOMPurify/tree/main/demos#what-is-this).
@@ -545,4 +565,4 @@ Feature releases will not be announced to this list.
545
565
 
546
566
  Many people have helped DOMPurify become what it is today, and they deserve to be acknowledged!
547
567
 
548
- [offset](https://github.com/offset), [Bankde](https://github.com/Bankde), [lukewarlow](https://github.com/lukewarlow), [DEMON1A](https://github.com/DEMON1A), [fg0x0](https://github.com/fg0x0), [kodareef5](https://github.com/kodareef5), [DavidOliver](https://github.com/DavidOliver), [1Jesper1](https://github.com/1Jesper1), [bencalif](https://github.com/bencalif), [trace37labs](https://github.com/trace37labs), [eddieran](https://github.com/eddieran), [christos-eth](https://github.com/christos-eth), [researchatfluidattacks](https://github.com/researchatfluidattacks), [frevadiscor](https://github.com/frevadiscor), [Rotzbua](https://github.com/Rotzbua), [binhpv](https://github.com/binhpv), [MariusRumpf](https://github.com/MariusRumpf), [prasadrajandran](https://github.com/prasadrajandran), [Cybozu 💛💸](https://github.com/cybozu), [hata6502 💸](https://github.com/hata6502), [openclaw 💸](https://github.com/openclaw), [intra-mart-dh 💸](https://github.com/intra-mart-dh), [nelstrom ❤️](https://github.com/nelstrom), [hash_kitten ❤️](https://twitter.com/hash_kitten), [kevin_mizu ❤️](https://twitter.com/kevin_mizu), [icesfont ❤️](https://github.com/icesfont), [reduckted ❤️](https://github.com/reduckted), [dcramer 💸](https://github.com/dcramer), [JGraph 💸](https://github.com/jgraph), [baekilda 💸](https://github.com/baekilda), [Healthchecks 💸](https://github.com/healthchecks), [Sentry 💸](https://github.com/getsentry), [jarrodldavis 💸](https://github.com/jarrodldavis), [CynegeticIO](https://github.com/CynegeticIO), [ssi02014 ❤️](https://github.com/ssi02014), [GrantGryczan](https://github.com/GrantGryczan), [Lowdefy](https://twitter.com/lowdefy), [granlem](https://twitter.com/MaximeVeit), [oreoshake](https://github.com/oreoshake), [tdeekens ❤️](https://github.com/tdeekens), [peernohell ❤️](https://github.com/peernohell), [is2ei](https://github.com/is2ei), [SoheilKhodayari](https://github.com/SoheilKhodayari), [franktopel](https://github.com/franktopel), [NateScarlet](https://github.com/NateScarlet), [neilj](https://github.com/neilj), [fhemberger](https://github.com/fhemberger), [Joris-van-der-Wel](https://github.com/Joris-van-der-Wel), [ydaniv](https://github.com/ydaniv), [terjanq](https://twitter.com/terjanq), [filedescriptor](https://github.com/filedescriptor), [ConradIrwin](https://github.com/ConradIrwin), [gibson042](https://github.com/gibson042), [choumx](https://github.com/choumx), [0xSobky](https://github.com/0xSobky), [styfle](https://github.com/styfle), [koto](https://github.com/koto), [tlau88](https://github.com/tlau88), [strugee](https://github.com/strugee), [oparoz](https://github.com/oparoz), [mathiasbynens](https://github.com/mathiasbynens), [edg2s](https://github.com/edg2s), [dnkolegov](https://github.com/dnkolegov), [dhardtke](https://github.com/dhardtke), [wirehead](https://github.com/wirehead), [thorn0](https://github.com/thorn0), [styu](https://github.com/styu), [mozfreddyb ❤️](https://github.com/mozfreddyb), [mikesamuel](https://github.com/mikesamuel), [jorangreef](https://github.com/jorangreef), [jimmyhchan](https://github.com/jimmyhchan), [jameydeorio](https://github.com/jameydeorio), [jameskraus](https://github.com/jameskraus), [hyderali](https://github.com/hyderali), [hansottowirtz](https://github.com/hansottowirtz), [hackvertor](https://github.com/hackvertor), [freddyb](https://github.com/freddyb), [flavorjones](https://github.com/flavorjones), [djfarrelly](https://github.com/djfarrelly), [devd](https://github.com/devd), [camerondunford](https://github.com/camerondunford), [buu700](https://github.com/buu700), [buildog](https://github.com/buildog), [alabiaga](https://github.com/alabiaga), [Vector919](https://github.com/Vector919), [Robbert](https://github.com/Robbert), [GreLI](https://github.com/GreLI), [FuzzySockets](https://github.com/FuzzySockets), [ArtemBernatskyy](https://github.com/ArtemBernatskyy), [@garethheyes](https://twitter.com/garethheyes), [@shafigullin](https://twitter.com/shafigullin), [@mmrupp](https://twitter.com/mmrupp), [@irsdl](https://twitter.com/irsdl),[ShikariSenpai](https://github.com/ShikariSenpai), [ansjdnakjdnajkd](https://github.com/ansjdnakjdnajkd), [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro), [@CmdEngineer\_](https://twitter.com/CmdEngineer_), [@avr4mit](https://twitter.com/avr4mit), [davecardwell](https://github.com/davecardwell) and especially [@securitymb ❤️](https://twitter.com/securitymb) & [@masatokinugawa ❤️](https://twitter.com/masatokinugawa)
568
+ [offset](https://github.com/offset), [Bankde](https://github.com/Bankde), [lukewarlow](https://github.com/lukewarlow), [DEMON1A](https://github.com/DEMON1A), [fg0x0](https://github.com/fg0x0), [kodareef5](https://github.com/kodareef5), [DavidOliver](https://github.com/DavidOliver), [1Jesper1](https://github.com/1Jesper1), [bencalif](https://github.com/bencalif), [trace37labs](https://github.com/trace37labs), [eddieran](https://github.com/eddieran), [christos-eth](https://github.com/christos-eth), [researchatfluidattacks](https://github.com/researchatfluidattacks), [frevadiscor](https://github.com/frevadiscor), [Rotzbua](https://github.com/Rotzbua), [binhpv](https://github.com/binhpv), [MariusRumpf](https://github.com/MariusRumpf), [prasadrajandran](https://github.com/prasadrajandran), [Cybozu 💛💸](https://github.com/cybozu), [hata6502 💸](https://github.com/hata6502), [openclaw 💸](https://github.com/openclaw), [intra-mart-dh 💸](https://github.com/intra-mart-dh), [nelstrom ❤️](https://github.com/nelstrom), [hash_kitten ❤️](https://twitter.com/hash_kitten), [kevin_mizu ❤️](https://twitter.com/kevin_mizu), [icesfont ❤️](https://github.com/icesfont), [reduckted ❤️](https://github.com/reduckted), [dcramer 💸](https://github.com/dcramer), [JGraph 💸](https://github.com/jgraph), [baekilda 💸](https://github.com/baekilda), [Healthchecks 💸](https://github.com/healthchecks), [Sentry 💸](https://github.com/getsentry), [jarrodldavis 💸](https://github.com/jarrodldavis), [CynegeticIO](https://github.com/CynegeticIO), [ssi02014 ❤️](https://github.com/ssi02014), [GrantGryczan](https://github.com/GrantGryczan), [Lowdefy](https://twitter.com/lowdefy), [granlem](https://twitter.com/MaximeVeit), [oreoshake](https://github.com/oreoshake), [tdeekens ❤️](https://github.com/tdeekens), [peernohell ❤️](https://github.com/peernohell), [is2ei](https://github.com/is2ei), [SoheilKhodayari](https://github.com/SoheilKhodayari), [franktopel](https://github.com/franktopel), [NateScarlet](https://github.com/NateScarlet), [neilj](https://github.com/neilj), [fhemberger](https://github.com/fhemberger), [Joris-van-der-Wel](https://github.com/Joris-van-der-Wel), [ydaniv](https://github.com/ydaniv), [terjanq](https://twitter.com/terjanq), [filedescriptor](https://github.com/filedescriptor), [ConradIrwin](https://github.com/ConradIrwin), [gibson042](https://github.com/gibson042), [choumx](https://github.com/choumx), [0xSobky](https://github.com/0xSobky), [styfle](https://github.com/styfle), [koto](https://github.com/koto), [tlau88](https://github.com/tlau88), [strugee](https://github.com/strugee), [oparoz](https://github.com/oparoz), [mathiasbynens](https://github.com/mathiasbynens), [edg2s](https://github.com/edg2s), [dnkolegov](https://github.com/dnkolegov), [dhardtke](https://github.com/dhardtke), [wirehead](https://github.com/wirehead), [thorn0](https://github.com/thorn0), [styu](https://github.com/styu), [mozfreddyb ❤️](https://github.com/mozfreddyb), [mikesamuel](https://github.com/mikesamuel), [jorangreef](https://github.com/jorangreef), [jimmyhchan](https://github.com/jimmyhchan), [jameydeorio](https://github.com/jameydeorio), [jameskraus](https://github.com/jameskraus), [hyderali](https://github.com/hyderali), [hansottowirtz](https://github.com/hansottowirtz), [hackvertor](https://github.com/hackvertor), [freddyb](https://github.com/freddyb), [flavorjones](https://github.com/flavorjones), [djfarrelly](https://github.com/djfarrelly), [devd](https://github.com/devd), [camerondunford](https://github.com/camerondunford), [buu700](https://github.com/buu700), [buildog](https://github.com/buildog), [alabiaga](https://github.com/alabiaga), [Vector919](https://github.com/Vector919), [Robbert](https://github.com/Robbert), [GreLI](https://github.com/GreLI), [FuzzySockets](https://github.com/FuzzySockets), [ArtemBernatskyy](https://github.com/ArtemBernatskyy), [@garethheyes](https://twitter.com/garethheyes), [@shafigullin](https://twitter.com/shafigullin), [@mmrupp](https://twitter.com/mmrupp), [@irsdl](https://twitter.com/irsdl),[ShikariSenpai](https://github.com/ShikariSenpai), [ansjdnakjdnajkd](https://github.com/ansjdnakjdnajkd), [@asutherland](https://twitter.com/asutherland), [@mathias](https://twitter.com/mathias), [@cgvwzq](https://twitter.com/cgvwzq), [@robbertatwork](https://twitter.com/robbertatwork), [@giutro](https://twitter.com/giutro), [@CmdEngineer\_](https://twitter.com/CmdEngineer_), [@avr4mit](https://twitter.com/avr4mit), [davecardwell](https://github.com/davecardwell), [Develop-KIM](https://github.com/Develop-KIM), [asamuzaK](https://github.com/asamuzaK), [fishjojo1](https://github.com/fishjojo1), [Rikuxx0](https://github.com/Rikuxx0) and especially [@securitymb ❤️](https://twitter.com/securitymb) & [@masatokinugawa ❤️](https://twitter.com/masatokinugawa)
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.10 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.10/LICENSE */
1
+ /*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */
2
2
 
3
3
  import { TrustedTypePolicy, TrustedTypesWindow, TrustedHTML } from 'trusted-types/lib/index.js';
4
4
 
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.10 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.10/LICENSE */
1
+ /*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
@@ -313,7 +313,7 @@ const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongd
313
313
  const text = freeze(['#text']);
314
314
 
315
315
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
316
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
316
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
317
317
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
318
318
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
319
319
 
@@ -337,7 +337,6 @@ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
337
337
  const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
338
338
  const SELF_CLOSING_TAG = seal(/\/>/i);
339
339
 
340
- /* eslint-disable @typescript-eslint/indent */
341
340
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
342
341
  const NODE_TYPE = {
343
342
  element: 1,
@@ -427,7 +426,7 @@ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options
427
426
  function createDOMPurify() {
428
427
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
429
428
  const DOMPurify = root => createDOMPurify(root);
430
- DOMPurify.version = '3.4.10';
429
+ DOMPurify.version = '3.4.12';
431
430
  DOMPurify.removed = [];
432
431
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
433
432
  // Not running in a browser, provide a factory function
@@ -616,6 +615,13 @@ function createDOMPurify() {
616
615
  let WHOLE_DOCUMENT = false;
617
616
  /* Track whether config is already set on this instance of DOMPurify. */
618
617
  let SET_CONFIG = false;
618
+ /* Pristine allowlist bindings captured at setConfig() time. On the
619
+ * persistent-config path sanitize() restores the sets from these before
620
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
621
+ * carry across calls. Null until setConfig() is called; reset by
622
+ * clearConfig(). */
623
+ let SET_CONFIG_ALLOWED_TAGS = null;
624
+ let SET_CONFIG_ALLOWED_ATTR = null;
619
625
  /* Decide if all elements (e.g. style, script) must be children of
620
626
  * document.body. By default, browsers might move them to document.head */
621
627
  let FORCE_BODY = false;
@@ -925,21 +931,6 @@ function createDOMPurify() {
925
931
  emptyHTML = _createTrustedHTML('');
926
932
  }
927
933
  }
928
- /*
929
- * Mirror the clone-before-mutate pattern already applied above for
930
- * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
931
- * registered AND the set still points at the default constant,
932
- * clone it. The hook then mutates the clone (in-call widening
933
- * still works exactly as documented) and the next default-cfg
934
- * call rebinds to the untouched original via the reassignment at
935
- * the top of this function.
936
- */
937
- if ((hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) && ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
938
- ALLOWED_TAGS = clone(ALLOWED_TAGS);
939
- }
940
- if (hooks.uponSanitizeAttribute.length > 0 && ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
941
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
942
- }
943
934
  // Prevent further manipulation of configuration.
944
935
  // Not available in IE8, Safari 5, etc.
945
936
  if (freeze) {
@@ -1117,6 +1108,13 @@ function createDOMPurify() {
1117
1108
  * @param root the in-place root to empty
1118
1109
  */
1119
1110
  const _neutralizeRoot = function _neutralizeRoot(root) {
1111
+ /* Strip every disallowed attribute (on* handlers included) off the whole
1112
+ subtree BEFORE detaching anything. Detaching first would hand back
1113
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
1114
+ whose queued resource event still fires in page scope after we throw.
1115
+ Clobber-safe reads; a doomed clobbered node's own attributes are
1116
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
1117
+ _neutralizeSubtree(root);
1120
1118
  const childNodes = getChildNodes(root);
1121
1119
  if (childNodes) {
1122
1120
  const snapshot = [];
@@ -1244,6 +1242,82 @@ function createDOMPurify() {
1244
1242
  }
1245
1243
  }
1246
1244
  };
1245
+ /**
1246
+ * _neutralizePatchLinkage
1247
+ *
1248
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1249
+ * hardening, https://github.com/WICG/declarative-partial-updates).
1250
+ *
1251
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1252
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
1253
+ * reaches each node. On a live in-place root that leaves a window: from the
1254
+ * moment the root is connected until the walk arrives at a given node, that
1255
+ * node's linkage is live. A patch applied on connection/stream can fire as
1256
+ * a microtask during the walk and inject or teleport an unsanitized DOM
1257
+ * range into a region the iterator has already passed and will not revisit,
1258
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
1259
+ * whole tree once up front and sever every linkage before the walk begins,
1260
+ * closing that window.
1261
+ *
1262
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
1263
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1264
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1265
+ * to group with the rest of the declarative-partial-updates handling and
1266
+ * stay overridable, consistent with the codebase.
1267
+ *
1268
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1269
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1270
+ *
1271
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1272
+ * .html Q1): this mirrors the existing policy of keeping `for` on
1273
+ * <label>/<output>. If the shipping feature can drive a patch through a
1274
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1275
+ * attribute check at _isBasicCustomElement's caller must additionally drop
1276
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1277
+ *
1278
+ * @param root the in-place root to sweep
1279
+ */
1280
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1281
+ if (!SAFE_FOR_XML) {
1282
+ return;
1283
+ }
1284
+ const stack = [root];
1285
+ while (stack.length > 0) {
1286
+ const node = stack.pop();
1287
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1288
+ /* Remove range markers (the target side of a patch linkage): every
1289
+ processing instruction, and any markup-bearing comment. */
1290
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1291
+ try {
1292
+ remove(node);
1293
+ } catch (_) {
1294
+ /* Best-effort */
1295
+ }
1296
+ continue;
1297
+ }
1298
+ /* Strip patch-source attributes (the source side) off elements. */
1299
+ if (nodeType === NODE_TYPE.element) {
1300
+ const element = node;
1301
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1302
+ try {
1303
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1304
+ element.removeAttribute('patchsrc');
1305
+ }
1306
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
1307
+ element.removeAttribute('for');
1308
+ }
1309
+ } catch (_) {
1310
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
1311
+ }
1312
+ }
1313
+ const childNodes = getChildNodes(node);
1314
+ if (childNodes) {
1315
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1316
+ stack.push(childNodes[i]);
1317
+ }
1318
+ }
1319
+ }
1320
+ };
1247
1321
  /**
1248
1322
  * _initDocument
1249
1323
  *
@@ -1494,9 +1568,15 @@ function createDOMPurify() {
1494
1568
  /**
1495
1569
  * Handle a node whose tag is forbidden or not allowlisted: keep
1496
1570
  * allowed custom elements (false return exits _sanitizeElements
1497
- * early - namespace/fallback checks and the afterSanitizeElements
1498
- * hook are intentionally skipped for kept custom elements), else
1499
- * hoist content per KEEP_CONTENT and remove.
1571
+ * early - the namespace and fallback-tag removal checks are
1572
+ * intentionally skipped for kept custom elements), else hoist
1573
+ * content per KEEP_CONTENT and remove.
1574
+ *
1575
+ * A kept custom element is the ONLY case in which this function
1576
+ * returns false, so the caller uses that return value to run the
1577
+ * afterSanitizeElements hook on the kept element and keep the
1578
+ * element-hook lifecycle consistent with normal allowlisted
1579
+ * elements (GHSA-c2j3-45gr-mqc4).
1500
1580
  *
1501
1581
  * @param currentNode the disallowed node
1502
1582
  * @param tagName the node's transformCaseFunc'd tag name
@@ -1562,9 +1642,15 @@ function createDOMPurify() {
1562
1642
  * @param currentNode to check for permission to exist
1563
1643
  * @return true if node was killed, false if left alive
1564
1644
  */
1565
- const _sanitizeElements = function _sanitizeElements(currentNode) {
1645
+ // eslint-disable-next-line complexity
1646
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1566
1647
  /* Execute a hook if present */
1567
1648
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1649
+ /* A hook may have detached the node — treat it as removed (see the
1650
+ detached-node comment after the uponSanitizeElement hook below). */
1651
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1652
+ return true;
1653
+ }
1568
1654
  /* Check if element is clobbered or can clobber */
1569
1655
  if (_isClobbered(currentNode)) {
1570
1656
  _forceRemove(currentNode);
@@ -1577,6 +1663,24 @@ function createDOMPurify() {
1577
1663
  tagName,
1578
1664
  allowedTags: ALLOWED_TAGS
1579
1665
  });
1666
+ /* A hook may have detached the node from the tree — a long-standing
1667
+ user pattern (issue #469; draw.io-style foreignObject filtering).
1668
+ Per the cached, unclobberable parentNode getter the node is
1669
+ genuinely out of the tree, so it can reach neither the serialized
1670
+ output nor an IN_PLACE live tree; treat it as removed and stop
1671
+ processing it. Without this guard, the unsafe-node / namespace
1672
+ checks below would call _forceRemove on a parentless node and hit
1673
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1674
+ wants gone but *cannot* detach (clobbered / parentless roots), the
1675
+ opposite of a node that is already safely gone. The walk root is
1676
+ exempt: a detached IN_PLACE root is legitimate input and must still
1677
+ be fully sanitized, and a kill-decision on it must keep hitting the
1678
+ REPORT-3 throw. Nodes detached by hooks are the hook's
1679
+ responsibility: they are not recorded in DOMPurify.removed and are
1680
+ not neutralized by the post-walk IN_PLACE pass. */
1681
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1682
+ return true;
1683
+ }
1580
1684
  /* Remove mXSS vectors, processing instructions and risky comments */
1581
1685
  if (_isUnsafeNode(currentNode, tagName)) {
1582
1686
  _forceRemove(currentNode);
@@ -1584,7 +1688,22 @@ function createDOMPurify() {
1584
1688
  }
1585
1689
  /* Remove element if anything forbids its presence */
1586
1690
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1587
- return _sanitizeDisallowedNode(currentNode, tagName);
1691
+ const removed = _sanitizeDisallowedNode(currentNode, tagName);
1692
+ /* A false return means the node is a custom element kept via
1693
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
1694
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
1695
+ element-hook lifecycle matches normal allowlisted elements: a
1696
+ security policy applied in this hook (e.g. stripping an attribute
1697
+ from every surviving element) must not silently skip kept custom
1698
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
1699
+ tail below - the hook runs, then the walker's subsequent
1700
+ _sanitizeAttributes pass sanitizes the element's attributes. The
1701
+ deliberately skipped namespace and fallback-tag removal checks stay
1702
+ skipped; they are removal decisions, not the hook contract. */
1703
+ if (removed === false) {
1704
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
1705
+ }
1706
+ return removed;
1588
1707
  }
1589
1708
  /* Check whether element has a valid namespace.
1590
1709
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -1631,6 +1750,33 @@ function createDOMPurify() {
1631
1750
  if (FORBID_ATTR[lcName]) {
1632
1751
  return false;
1633
1752
  }
1753
+ /* Reject declarative-partial-updates patch-linkage attributes
1754
+ (https://github.com/WICG/declarative-partial-updates).
1755
+ Empirical note (Chrome 150, verified — see
1756
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
1757
+ sanitization. For the string path it fires during sanitize()'s own
1758
+ parse, so the walk sees and sanitizes the fully materialized expanded
1759
+ tree — teleports into MathML/SVG integration points included; a
1760
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
1761
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
1762
+ Either way DOMPurify is NOT blind to the patch.
1763
+ This removal is therefore defense-in-depth rather than the sole barrier:
1764
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
1765
+ in the caller's context, and keeps behaviour deterministic if a future
1766
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
1767
+ anywhere else (notably <template for>) it links the element to a patch
1768
+ target and teleports or removes an arbitrary DOM range by id/marker name.
1769
+ `patchsrc` fetches remote markup and is treated as a script-loading
1770
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
1771
+ other structural-threat checks and stays overridable, consistent with
1772
+ the rest of the codebase. PI range markers are already removed by
1773
+ _isUnsafeNode. */
1774
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
1775
+ return false;
1776
+ }
1777
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
1778
+ return false;
1779
+ }
1634
1780
  /* Make sure attribute cannot clobber */
1635
1781
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1636
1782
  return false;
@@ -1840,7 +1986,7 @@ function createDOMPurify() {
1840
1986
  /* Execute a hook if present */
1841
1987
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
1842
1988
  /* Sanitize tags and elements */
1843
- _sanitizeElements(shadowNode);
1989
+ _sanitizeElements(shadowNode, fragment);
1844
1990
  /* Check attributes next */
1845
1991
  _sanitizeAttributes(shadowNode);
1846
1992
  /* Deep shadow DOM detected.
@@ -1996,9 +2142,31 @@ function createDOMPurify() {
1996
2142
  return dirty;
1997
2143
  }
1998
2144
  /* Assign config vars */
1999
- if (!SET_CONFIG) {
2145
+ if (SET_CONFIG) {
2146
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
2147
+ * not re-derived per call. Restore them from the pristine bindings
2148
+ * captured at setConfig() time so a previous call's hook clone (mutated
2149
+ * below) does not carry over. */
2150
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
2151
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
2152
+ } else {
2000
2153
  _parseConfig(cfg);
2001
2154
  }
2155
+ /* Clone the hook-mutable allowlists before the walk whenever an
2156
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2157
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2158
+ * a hook that widens them would otherwise mutate the shared set
2159
+ * permanently: across later calls and across every element. Cloning per
2160
+ * walk keeps documented in-call widening working while scoping it to the
2161
+ * call. A single guard for both config paths - the per-call path rebinds
2162
+ * the sets in _parseConfig each call, the persistent path restores them
2163
+ * from the captured bindings just above - so the two cannot diverge. */
2164
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
2165
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2166
+ }
2167
+ if (hooks.uponSanitizeAttribute.length > 0) {
2168
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2169
+ }
2002
2170
  /* Clean up removed elements */
2003
2171
  DOMPurify.removed = [];
2004
2172
  /* Resolve IN_PLACE for this call without mutating persistent config.
@@ -2009,6 +2177,11 @@ function createDOMPurify() {
2009
2177
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
2010
2178
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2011
2179
  if (inPlace) {
2180
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
2181
+ linkage across the live tree BEFORE the walk, so no patch can fire
2182
+ mid-walk and inject into an already-processed region. Runs first, so
2183
+ it also covers the forbidden/clobbered roots that throw below. */
2184
+ _neutralizePatchLinkage(dirty);
2012
2185
  /* Do some early pre-sanitization to avoid unsafe root nodes.
2013
2186
  Read nodeName through the cached prototype getter — a clobbering
2014
2187
  child named "nodeName" on the form root would otherwise shadow
@@ -2018,6 +2191,9 @@ function createDOMPurify() {
2018
2191
  if (typeof nn === 'string') {
2019
2192
  const tagName = transformCaseFunc(nn);
2020
2193
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
2194
+ /* Fail closed on a live root: neutralize handlers/children before
2195
+ throwing, exactly as the mid-walk abort path does. */
2196
+ _neutralizeRoot(dirty);
2021
2197
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
2022
2198
  }
2023
2199
  }
@@ -2032,6 +2208,10 @@ function createDOMPurify() {
2032
2208
  the application unsanitized. Refuse to sanitize such a root
2033
2209
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2034
2210
  if (_isClobbered(dirty)) {
2211
+ /* Fail closed on a live clobbered root before throwing.
2212
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
2213
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
2214
+ _neutralizeRoot(dirty);
2035
2215
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
2036
2216
  }
2037
2217
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -2084,7 +2264,8 @@ function createDOMPurify() {
2084
2264
  _forceRemove(body.firstChild);
2085
2265
  }
2086
2266
  /* Get node iterator */
2087
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
2267
+ const walkRoot = inPlace ? dirty : body;
2268
+ const nodeIterator = _createNodeIterator(walkRoot);
2088
2269
  /* Now start iterating over the created document.
2089
2270
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2090
2271
  engine/custom-element mutation can detach a node mid-walk so
@@ -2097,7 +2278,7 @@ function createDOMPurify() {
2097
2278
  try {
2098
2279
  while (currentNode = nodeIterator.nextNode()) {
2099
2280
  /* Sanitize tags and elements */
2100
- _sanitizeElements(currentNode);
2281
+ _sanitizeElements(currentNode, walkRoot);
2101
2282
  /* Check attributes next */
2102
2283
  _sanitizeAttributes(currentNode);
2103
2284
  /* Shadow DOM detected, sanitize it.
@@ -2111,6 +2292,14 @@ function createDOMPurify() {
2111
2292
  } catch (error) {
2112
2293
  if (inPlace) {
2113
2294
  _neutralizeRoot(dirty);
2295
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
2296
+ detached from the root, so _neutralizeRoot's subtree pass does not
2297
+ reach them. Defuse them too, mirroring the success-path loop below. */
2298
+ arrayForEach(DOMPurify.removed, entry => {
2299
+ if (entry.element) {
2300
+ _neutralizeSubtree(entry.element);
2301
+ }
2302
+ });
2114
2303
  }
2115
2304
  throw error;
2116
2305
  }
@@ -2174,10 +2363,14 @@ function createDOMPurify() {
2174
2363
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
2175
2364
  _parseConfig(cfg);
2176
2365
  SET_CONFIG = true;
2366
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2367
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
2177
2368
  };
2178
2369
  DOMPurify.clearConfig = function () {
2179
2370
  CONFIG = null;
2180
2371
  SET_CONFIG = false;
2372
+ SET_CONFIG_ALLOWED_TAGS = null;
2373
+ SET_CONFIG_ALLOWED_ATTR = null;
2181
2374
  // Drop any caller-supplied Trusted Types policy so it cannot poison later
2182
2375
  // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
2183
2376
  // never recreated — Trusted Types throws on duplicate names) is restored by
@@ -2198,9 +2391,19 @@ function createDOMPurify() {
2198
2391
  if (typeof hookFunction !== 'function') {
2199
2392
  return;
2200
2393
  }
2394
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2395
+ * '__proto__') indexes off the prototype chain rather than a real
2396
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2397
+ * with an own-property check against the known hook names. */
2398
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2399
+ return;
2400
+ }
2201
2401
  arrayPush(hooks[entryPoint], hookFunction);
2202
2402
  };
2203
2403
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
2404
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2405
+ return undefined;
2406
+ }
2204
2407
  if (hookFunction !== undefined) {
2205
2408
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
2206
2409
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -2208,6 +2411,9 @@ function createDOMPurify() {
2208
2411
  return arrayPop(hooks[entryPoint]);
2209
2412
  };
2210
2413
  DOMPurify.removeHooks = function (entryPoint) {
2414
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2415
+ return;
2416
+ }
2211
2417
  hooks[entryPoint] = [];
2212
2418
  };
2213
2419
  DOMPurify.removeAllHooks = function () {