dompurify 3.4.11 → 3.4.13

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.11**.
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.13**.
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
 
@@ -565,4 +565,4 @@ Feature releases will not be announced to this list.
565
565
 
566
566
  Many people have helped DOMPurify become what it is today, and they deserve to be acknowledged!
567
567
 
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) 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.11 | (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.11/LICENSE */
1
+ /*! @license DOMPurify 3.4.13 | (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.13/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.11 | (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.11/LICENSE */
1
+ /*! @license DOMPurify 3.4.13 | (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.13/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
 
@@ -426,7 +426,7 @@ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options
426
426
  function createDOMPurify() {
427
427
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
428
428
  const DOMPurify = root => createDOMPurify(root);
429
- DOMPurify.version = '3.4.11';
429
+ DOMPurify.version = '3.4.13';
430
430
  DOMPurify.removed = [];
431
431
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
432
432
  // Not running in a browser, provide a factory function
@@ -457,6 +457,7 @@ function createDOMPurify() {
457
457
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
458
458
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
459
459
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
460
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
460
461
  // As per issue #47, the web-components registry is inherited by a
461
462
  // new document created via createHTMLDocument. As per the spec
462
463
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -1108,6 +1109,13 @@ function createDOMPurify() {
1108
1109
  * @param root the in-place root to empty
1109
1110
  */
1110
1111
  const _neutralizeRoot = function _neutralizeRoot(root) {
1112
+ /* Strip every disallowed attribute (on* handlers included) off the whole
1113
+ subtree BEFORE detaching anything. Detaching first would hand back
1114
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
1115
+ whose queued resource event still fires in page scope after we throw.
1116
+ Clobber-safe reads; a doomed clobbered node's own attributes are
1117
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
1118
+ _neutralizeSubtree(root);
1111
1119
  const childNodes = getChildNodes(root);
1112
1120
  if (childNodes) {
1113
1121
  const snapshot = [];
@@ -1235,6 +1243,82 @@ function createDOMPurify() {
1235
1243
  }
1236
1244
  }
1237
1245
  };
1246
+ /**
1247
+ * _neutralizePatchLinkage
1248
+ *
1249
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1250
+ * hardening, https://github.com/WICG/declarative-partial-updates).
1251
+ *
1252
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1253
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
1254
+ * reaches each node. On a live in-place root that leaves a window: from the
1255
+ * moment the root is connected until the walk arrives at a given node, that
1256
+ * node's linkage is live. A patch applied on connection/stream can fire as
1257
+ * a microtask during the walk and inject or teleport an unsanitized DOM
1258
+ * range into a region the iterator has already passed and will not revisit,
1259
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
1260
+ * whole tree once up front and sever every linkage before the walk begins,
1261
+ * closing that window.
1262
+ *
1263
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
1264
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1265
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1266
+ * to group with the rest of the declarative-partial-updates handling and
1267
+ * stay overridable, consistent with the codebase.
1268
+ *
1269
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1270
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1271
+ *
1272
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1273
+ * .html Q1): this mirrors the existing policy of keeping `for` on
1274
+ * <label>/<output>. If the shipping feature can drive a patch through a
1275
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1276
+ * attribute check at _isBasicCustomElement's caller must additionally drop
1277
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1278
+ *
1279
+ * @param root the in-place root to sweep
1280
+ */
1281
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1282
+ if (!SAFE_FOR_XML) {
1283
+ return;
1284
+ }
1285
+ const stack = [root];
1286
+ while (stack.length > 0) {
1287
+ const node = stack.pop();
1288
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1289
+ /* Remove range markers (the target side of a patch linkage): every
1290
+ processing instruction, and any markup-bearing comment. */
1291
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1292
+ try {
1293
+ remove(node);
1294
+ } catch (_) {
1295
+ /* Best-effort */
1296
+ }
1297
+ continue;
1298
+ }
1299
+ /* Strip patch-source attributes (the source side) off elements. */
1300
+ if (nodeType === NODE_TYPE.element) {
1301
+ const element = node;
1302
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1303
+ try {
1304
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1305
+ element.removeAttribute('patchsrc');
1306
+ }
1307
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
1308
+ element.removeAttribute('for');
1309
+ }
1310
+ } catch (_) {
1311
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
1312
+ }
1313
+ }
1314
+ const childNodes = getChildNodes(node);
1315
+ if (childNodes) {
1316
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1317
+ stack.push(childNodes[i]);
1318
+ }
1319
+ }
1320
+ }
1321
+ };
1238
1322
  /**
1239
1323
  * _initDocument
1240
1324
  *
@@ -1292,7 +1376,17 @@ function createDOMPurify() {
1292
1376
  * @return The created NodeIterator
1293
1377
  */
1294
1378
  const _createNodeIterator = function _createNodeIterator(root) {
1295
- return createNodeIterator.call(root.ownerDocument || root, root,
1379
+ /* Read ownerDocument through the cached Node.prototype getter, never the
1380
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
1381
+ clobbering child (<input name="ownerDocument"> or a form-associated
1382
+ external input) shadows the prototype getter and makes a direct read
1383
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
1384
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
1385
+ the walk's fail-closed barrier - leaving the caller's live tree, with
1386
+ any already-armed handler in it, un-neutralized. The cached getter
1387
+ returns the real Document regardless of the clobber. */
1388
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
1389
+ return createNodeIterator.call(doc || root, root,
1296
1390
  // eslint-disable-next-line no-bitwise
1297
1391
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1298
1392
  };
@@ -1332,7 +1426,11 @@ function createDOMPurify() {
1332
1426
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1333
1427
  var _node$querySelectorAl;
1334
1428
  node.normalize();
1335
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
1429
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
1430
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
1431
+ carry a form-named-getter override of ownerDocument. */
1432
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1433
+ const walker = createNodeIterator.call(doc || node, node,
1336
1434
  // eslint-disable-next-line no-bitwise
1337
1435
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1338
1436
  let currentNode = walker.nextNode();
@@ -1485,15 +1583,21 @@ function createDOMPurify() {
1485
1583
  /**
1486
1584
  * Handle a node whose tag is forbidden or not allowlisted: keep
1487
1585
  * allowed custom elements (false return exits _sanitizeElements
1488
- * early - namespace/fallback checks and the afterSanitizeElements
1489
- * hook are intentionally skipped for kept custom elements), else
1490
- * hoist content per KEEP_CONTENT and remove.
1586
+ * early - the namespace and fallback-tag removal checks are
1587
+ * intentionally skipped for kept custom elements), else hoist
1588
+ * content per KEEP_CONTENT and remove.
1589
+ *
1590
+ * A kept custom element is the ONLY case in which this function
1591
+ * returns false, so the caller uses that return value to run the
1592
+ * afterSanitizeElements hook on the kept element and keep the
1593
+ * element-hook lifecycle consistent with normal allowlisted
1594
+ * elements (GHSA-c2j3-45gr-mqc4).
1491
1595
  *
1492
1596
  * @param currentNode the disallowed node
1493
1597
  * @param tagName the node's transformCaseFunc'd tag name
1494
1598
  * @return true if the node was removed, false if kept
1495
1599
  */
1496
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1600
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1497
1601
  /* Check if we have a custom element to handle */
1498
1602
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1499
1603
  if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -1516,27 +1620,26 @@ function createDOMPurify() {
1516
1620
  const childNodes = getChildNodes(currentNode);
1517
1621
  if (childNodes && parentNode) {
1518
1622
  const childCount = childNodes.length;
1519
- /* In-place: hoist the *original* children so the iterator visits
1520
- and sanitises them through the same allowlist pass as every other
1521
- node. The caller built the tree in the live document, so the
1522
- originals carry already-queued resource events (`<img onerror>`,
1523
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
1524
- those originals detached but still armed, firing in page scope
1525
- while the returned tree looked clean. Moving is safe in-place: the
1526
- root is pre-validated as an allowed tag and so is never the node
1527
- being removed, which keeps `parentNode` inside the iterator root
1528
- and the relocated child inside the serialised tree.
1529
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1530
- at and the result serialised from `body`, so a restrictive
1531
- ALLOWED_TAGS that removes `body` itself must leave its content in
1532
- place, which only cloning does; and those paths parse into an
1533
- inert document, so their discarded originals never had a queued
1534
- event to neutralise.
1623
+ /* Hoist by moving each child up one level rather than deep-cloning
1624
+ it. Moving transfers every descendant exactly once, so a chain of
1625
+ nested disallowed elements costs O(n) instead of the O(n^2) that
1626
+ re-cloning the shrinking subtree at each level produced; it also
1627
+ empties the removed original, so `DOMPurify.removed` no longer
1628
+ pins whole subtrees. Moving preserves the in-place guarantee too:
1629
+ an original carrying already-queued resource events (`<img
1630
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
1631
+ relocated and sanitised rather than left detached but still armed.
1632
+ The sole case that must clone is removing the walk root itself.
1633
+ The result is serialised from the root's subtree, so a restrictive
1634
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
1635
+ leave the content inside it, which only cloning does. In IN_PLACE
1636
+ the root is pre-validated as an allowed tag and so is never removed
1637
+ here, so that path always takes the move branch.
1535
1638
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1536
1639
  valid whether we move (drops the trailing entry) or clone (leaves
1537
1640
  the list intact). */
1538
1641
  for (let i = childCount - 1; i >= 0; --i) {
1539
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1642
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
1540
1643
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1541
1644
  }
1542
1645
  }
@@ -1544,6 +1647,26 @@ function createDOMPurify() {
1544
1647
  _forceRemove(currentNode);
1545
1648
  return true;
1546
1649
  };
1650
+ /**
1651
+ * Fork a hook-mutable allowlist off its shared binding the first time a
1652
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
1653
+ * hook cannot widen the per-instance default or the setConfig binding by
1654
+ * reference and leak past the call. Returns the set unchanged once it is
1655
+ * already call-local, so repeated calls across elements are idempotent.
1656
+ *
1657
+ * @param hookList the uponSanitize* hook array for this event
1658
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
1659
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
1660
+ * @param setConfigSet the captured setConfig() binding, or null
1661
+ * @return a call-local clone if a hook is present and set is still shared,
1662
+ * else set unchanged
1663
+ */
1664
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
1665
+ if (hookList.length === 0) {
1666
+ return set;
1667
+ }
1668
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
1669
+ };
1547
1670
  /**
1548
1671
  * _sanitizeElements
1549
1672
  *
@@ -1553,9 +1676,20 @@ function createDOMPurify() {
1553
1676
  * @param currentNode to check for permission to exist
1554
1677
  * @return true if node was killed, false if left alive
1555
1678
  */
1556
- const _sanitizeElements = function _sanitizeElements(currentNode) {
1679
+ // eslint-disable-next-line complexity
1680
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1557
1681
  /* Execute a hook if present */
1558
1682
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1683
+ /* A hook may have detached the node - treat it as removed (see the
1684
+ detached-node comment after the uponSanitizeElement hook below). On
1685
+ the IN_PLACE path, neutralize the detached subtree first so a queued
1686
+ resource handler on it cannot fire in page scope after we return. */
1687
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1688
+ if (IN_PLACE) {
1689
+ _neutralizeSubtree(currentNode);
1690
+ }
1691
+ return true;
1692
+ }
1559
1693
  /* Check if element is clobbered or can clobber */
1560
1694
  if (_isClobbered(currentNode)) {
1561
1695
  _forceRemove(currentNode);
@@ -1563,11 +1697,47 @@ function createDOMPurify() {
1563
1697
  }
1564
1698
  /* Now let's check the element's type and name */
1565
1699
  const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1700
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
1701
+ hook may have been installed after that guard sampled the hook arrays
1702
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
1703
+ aliasing a shared binding that a widening hook would mutate by
1704
+ reference. Fork it before exposing it to the hook. */
1705
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
1566
1706
  /* Execute a hook if present */
1567
1707
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1568
1708
  tagName,
1569
1709
  allowedTags: ALLOWED_TAGS
1570
1710
  });
1711
+ /* A hook may have detached the node from the tree — a long-standing
1712
+ user pattern (issue #469; draw.io-style foreignObject filtering).
1713
+ Per the cached, unclobberable parentNode getter the node is
1714
+ genuinely out of the tree, so it can reach neither the serialized
1715
+ output nor an IN_PLACE live tree; treat it as removed and stop
1716
+ processing it. Without this guard, the unsafe-node / namespace
1717
+ checks below would call _forceRemove on a parentless node and hit
1718
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1719
+ wants gone but *cannot* detach (clobbered / parentless roots), the
1720
+ opposite of a node that is already safely gone. The walk root is
1721
+ exempt: a detached IN_PLACE root is legitimate input and must still
1722
+ be fully sanitized, and a kill-decision on it must keep hitting the
1723
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
1724
+ responsibility for placement: they are not recorded in
1725
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
1726
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
1727
+ can still hold a queued resource-event handler - e.g. an <img onload>
1728
+ that began loading when the caller built the live tree - which fires
1729
+ in page scope after sanitize returns even though the handler never
1730
+ reached the returned tree. That is the audit-5 F1 hazard, and the
1731
+ documented node.remove() hook pattern walks straight into it. So on
1732
+ the IN_PLACE path we neutralize the detached subtree inline here,
1733
+ stripping its non-allow-listed attributes before returning, exactly
1734
+ as the post-walk pass does for _forceRemove'd subtrees. */
1735
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1736
+ if (IN_PLACE) {
1737
+ _neutralizeSubtree(currentNode);
1738
+ }
1739
+ return true;
1740
+ }
1571
1741
  /* Remove mXSS vectors, processing instructions and risky comments */
1572
1742
  if (_isUnsafeNode(currentNode, tagName)) {
1573
1743
  _forceRemove(currentNode);
@@ -1575,7 +1745,22 @@ function createDOMPurify() {
1575
1745
  }
1576
1746
  /* Remove element if anything forbids its presence */
1577
1747
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1578
- return _sanitizeDisallowedNode(currentNode, tagName);
1748
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
1749
+ /* A false return means the node is a custom element kept via
1750
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
1751
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
1752
+ element-hook lifecycle matches normal allowlisted elements: a
1753
+ security policy applied in this hook (e.g. stripping an attribute
1754
+ from every surviving element) must not silently skip kept custom
1755
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
1756
+ tail below - the hook runs, then the walker's subsequent
1757
+ _sanitizeAttributes pass sanitizes the element's attributes. The
1758
+ deliberately skipped namespace and fallback-tag removal checks stay
1759
+ skipped; they are removal decisions, not the hook contract. */
1760
+ if (removed === false) {
1761
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
1762
+ }
1763
+ return removed;
1579
1764
  }
1580
1765
  /* Check whether element has a valid namespace.
1581
1766
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -1622,6 +1807,33 @@ function createDOMPurify() {
1622
1807
  if (FORBID_ATTR[lcName]) {
1623
1808
  return false;
1624
1809
  }
1810
+ /* Reject declarative-partial-updates patch-linkage attributes
1811
+ (https://github.com/WICG/declarative-partial-updates).
1812
+ Empirical note (Chrome 150, verified — see
1813
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
1814
+ sanitization. For the string path it fires during sanitize()'s own
1815
+ parse, so the walk sees and sanitizes the fully materialized expanded
1816
+ tree — teleports into MathML/SVG integration points included; a
1817
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
1818
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
1819
+ Either way DOMPurify is NOT blind to the patch.
1820
+ This removal is therefore defense-in-depth rather than the sole barrier:
1821
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
1822
+ in the caller's context, and keeps behaviour deterministic if a future
1823
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
1824
+ anywhere else (notably <template for>) it links the element to a patch
1825
+ target and teleports or removes an arbitrary DOM range by id/marker name.
1826
+ `patchsrc` fetches remote markup and is treated as a script-loading
1827
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
1828
+ other structural-threat checks and stays overridable, consistent with
1829
+ the rest of the codebase. PI range markers are already removed by
1830
+ _isUnsafeNode. */
1831
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
1832
+ return false;
1833
+ }
1834
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
1835
+ return false;
1836
+ }
1625
1837
  /* Make sure attribute cannot clobber */
1626
1838
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1627
1839
  return false;
@@ -1738,6 +1950,9 @@ function createDOMPurify() {
1738
1950
  if (!attributes || _isClobbered(currentNode)) {
1739
1951
  return;
1740
1952
  }
1953
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
1954
+ attribute allowlist off its shared binding before a hook can see it. */
1955
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
1741
1956
  const hookEvent = {
1742
1957
  attrName: '',
1743
1958
  attrValue: '',
@@ -1831,7 +2046,7 @@ function createDOMPurify() {
1831
2046
  /* Execute a hook if present */
1832
2047
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
1833
2048
  /* Sanitize tags and elements */
1834
- _sanitizeElements(shadowNode);
2049
+ _sanitizeElements(shadowNode, fragment);
1835
2050
  /* Check attributes next */
1836
2051
  _sanitizeAttributes(shadowNode);
1837
2052
  /* Deep shadow DOM detected.
@@ -2022,6 +2237,11 @@ function createDOMPurify() {
2022
2237
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
2023
2238
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2024
2239
  if (inPlace) {
2240
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
2241
+ linkage across the live tree BEFORE the walk, so no patch can fire
2242
+ mid-walk and inject into an already-processed region. Runs first, so
2243
+ it also covers the forbidden/clobbered roots that throw below. */
2244
+ _neutralizePatchLinkage(dirty);
2025
2245
  /* Do some early pre-sanitization to avoid unsafe root nodes.
2026
2246
  Read nodeName through the cached prototype getter — a clobbering
2027
2247
  child named "nodeName" on the form root would otherwise shadow
@@ -2031,6 +2251,9 @@ function createDOMPurify() {
2031
2251
  if (typeof nn === 'string') {
2032
2252
  const tagName = transformCaseFunc(nn);
2033
2253
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
2254
+ /* Fail closed on a live root: neutralize handlers/children before
2255
+ throwing, exactly as the mid-walk abort path does. */
2256
+ _neutralizeRoot(dirty);
2034
2257
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
2035
2258
  }
2036
2259
  }
@@ -2045,6 +2268,10 @@ function createDOMPurify() {
2045
2268
  the application unsanitized. Refuse to sanitize such a root
2046
2269
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2047
2270
  if (_isClobbered(dirty)) {
2271
+ /* Fail closed on a live clobbered root before throwing.
2272
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
2273
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
2274
+ _neutralizeRoot(dirty);
2048
2275
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
2049
2276
  }
2050
2277
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -2097,20 +2324,24 @@ function createDOMPurify() {
2097
2324
  _forceRemove(body.firstChild);
2098
2325
  }
2099
2326
  /* Get node iterator */
2100
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
2327
+ const walkRoot = inPlace ? dirty : body;
2101
2328
  /* Now start iterating over the created document.
2102
2329
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2103
2330
  engine/custom-element mutation can detach a node mid-walk so
2104
2331
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2105
2332
  barrier the caller's in-place tree would be left half-sanitized with the
2106
- unvisited tail still armed. On any throw we fail closed — strip the
2107
- in-place root bare then rethrow so the existing throw contract is
2108
- preserved. (String/DOM-copy paths never return the partial body, so the
2109
- propagating throw is already fail-closed there.) */
2333
+ unvisited tail still armed. _createNodeIterator itself is inside the
2334
+ barrier too: constructing the iterator dereferences the root's document,
2335
+ and any failure there (e.g. an exotic/clobbered root) must still fail
2336
+ closed rather than skip the neutralize. On any throw we fail closed -
2337
+ strip the in-place root bare - then rethrow so the existing throw
2338
+ contract is preserved. (String/DOM-copy paths never return the partial
2339
+ body, so the propagating throw is already fail-closed there.) */
2110
2340
  try {
2341
+ const nodeIterator = _createNodeIterator(walkRoot);
2111
2342
  while (currentNode = nodeIterator.nextNode()) {
2112
2343
  /* Sanitize tags and elements */
2113
- _sanitizeElements(currentNode);
2344
+ _sanitizeElements(currentNode, walkRoot);
2114
2345
  /* Check attributes next */
2115
2346
  _sanitizeAttributes(currentNode);
2116
2347
  /* Shadow DOM detected, sanitize it.
@@ -2124,6 +2355,14 @@ function createDOMPurify() {
2124
2355
  } catch (error) {
2125
2356
  if (inPlace) {
2126
2357
  _neutralizeRoot(dirty);
2358
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
2359
+ detached from the root, so _neutralizeRoot's subtree pass does not
2360
+ reach them. Defuse them too, mirroring the success-path loop below. */
2361
+ arrayForEach(DOMPurify.removed, entry => {
2362
+ if (entry.element) {
2363
+ _neutralizeSubtree(entry.element);
2364
+ }
2365
+ });
2127
2366
  }
2128
2367
  throw error;
2129
2368
  }