dompurify 3.4.2 → 3.4.4

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
@@ -1,18 +1,18 @@
1
1
  # DOMPurify
2
2
 
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) ![Tests](https://github.com/cure53/DOMPurify/workflows/Build%20&%20Test/badge.svg) [![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)
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)
4
4
 
5
- ![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) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12162/badge)](https://www.bestpractices.dev/projects/12162) [![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)
5
+ [![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 Best Practices](https://www.bestpractices.dev/projects/12162/badge)](https://www.bestpractices.dev/projects/12162) [![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) [![Cloudback](https://app.cloudback.it/badge/cure53/DOMPurify)](https://cloudback.it)
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.2**.
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.4**.
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
 
13
13
  **Note that [DOMPurify v2.5.9](https://github.com/cure53/DOMPurify/releases/tag/2.5.9) is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the [2.x branch](https://github.com/cure53/DOMPurify/tree/2.x).**
14
14
 
15
- Our automated tests cover 9 browser/OS combinations (Chromium, Firefox, and WebKit across Ubuntu, macOS, and Windows) on every push, plus Node.js v20, v22, v24, and v25 running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees.
15
+ Our automated tests cover 9 browser/OS combinations (Chromium, Firefox, and WebKit across Ubuntu, macOS, and Windows) on every push, plus Node.js v20, v22, v24, v25 and v26 running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees.
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.
18
18
 
@@ -463,7 +463,7 @@ DOMPurify.addHook(
463
463
 
464
464
  ## Continuous Integration
465
465
 
466
- We are currently using GitHub Actions in combination with BrowserStack. This gives us the possibility to confirm for each and every commit that all is going according to plan in all supported browsers. Check out the build logs here: https://github.com/cure53/DOMPurify/actions
466
+ We are currently using GitHub Actions in combination with Playwright. This gives us the possibility to confirm for each and every commit that all is going according to plan in relevant modern browsers. Check out the build logs here: https://github.com/cure53/DOMPurify/actions
467
467
 
468
468
  You can further run local tests by executing `npm run test`.
469
469
 
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.2 | (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.2/LICENSE */
1
+ /*! @license DOMPurify 3.4.4 | (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.4/LICENSE */
2
2
 
3
3
  import { TrustedTypePolicy, TrustedTypesWindow, TrustedHTML } from 'trusted-types/lib/index.js';
4
4
 
@@ -1,23 +1,64 @@
1
- /*! @license DOMPurify 3.4.2 | (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.2/LICENSE */
1
+ /*! @license DOMPurify 3.4.4 | (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.4/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
5
- const {
6
- entries,
7
- setPrototypeOf,
8
- isFrozen,
9
- getPrototypeOf,
10
- getOwnPropertyDescriptor
11
- } = Object;
12
- let {
13
- freeze,
14
- seal,
15
- create
16
- } = Object; // eslint-disable-line import/no-mutable-exports
17
- let {
18
- apply,
19
- construct
20
- } = typeof Reflect !== 'undefined' && Reflect;
5
+ function _arrayLikeToArray(r, a) {
6
+ (null == a || a > r.length) && (a = r.length);
7
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
8
+ return n;
9
+ }
10
+ function _arrayWithHoles(r) {
11
+ if (Array.isArray(r)) return r;
12
+ }
13
+ function _iterableToArrayLimit(r, l) {
14
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
15
+ if (null != t) {
16
+ var e,
17
+ n,
18
+ i,
19
+ u,
20
+ a = [],
21
+ f = true,
22
+ o = false;
23
+ try {
24
+ if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
25
+ } catch (r) {
26
+ o = true, n = r;
27
+ } finally {
28
+ try {
29
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
30
+ } finally {
31
+ if (o) throw n;
32
+ }
33
+ }
34
+ return a;
35
+ }
36
+ }
37
+ function _nonIterableRest() {
38
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
39
+ }
40
+ function _slicedToArray(r, e) {
41
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
42
+ }
43
+ function _unsupportedIterableToArray(r, a) {
44
+ if (r) {
45
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
46
+ var t = {}.toString.call(r).slice(8, -1);
47
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
48
+ }
49
+ }
50
+
51
+ const entries = Object.entries,
52
+ setPrototypeOf = Object.setPrototypeOf,
53
+ isFrozen = Object.isFrozen,
54
+ getPrototypeOf = Object.getPrototypeOf,
55
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
56
+ let freeze = Object.freeze,
57
+ seal = Object.seal,
58
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
59
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
60
+ apply = _ref.apply,
61
+ construct = _ref.construct;
21
62
  if (!freeze) {
22
63
  freeze = function freeze(x) {
23
64
  return x;
@@ -154,7 +195,10 @@ function cleanArray(array) {
154
195
  */
155
196
  function clone(object) {
156
197
  const newObject = create(null);
157
- for (const [property, value] of entries(object)) {
198
+ for (const _ref2 of entries(object)) {
199
+ var _ref3 = _slicedToArray(_ref2, 2);
200
+ const property = _ref3[0];
201
+ const value = _ref3[1];
158
202
  const isPropertyExist = objectHasOwnProperty(object, property);
159
203
  if (isPropertyExist) {
160
204
  if (arrayIsArray(value)) {
@@ -254,7 +298,7 @@ function isRegex(value) {
254
298
  }
255
299
  }
256
300
 
257
- const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
301
+ const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
258
302
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
259
303
  const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
260
304
  // List of SVG elements that are disallowed by default.
@@ -268,15 +312,14 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
268
312
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
269
313
  const text = freeze(['#text']);
270
314
 
271
- const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', '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']);
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']);
272
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']);
273
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']);
274
318
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
275
319
 
276
- // eslint-disable-next-line unicorn/better-regex
277
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
278
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
279
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
320
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
321
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
322
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
280
323
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
281
324
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
282
325
  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
@@ -287,20 +330,6 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
287
330
  const DOCTYPE_NAME = seal(/^html$/i);
288
331
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
289
332
 
290
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
291
- __proto__: null,
292
- ARIA_ATTR: ARIA_ATTR,
293
- ATTR_WHITESPACE: ATTR_WHITESPACE,
294
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
295
- DATA_ATTR: DATA_ATTR,
296
- DOCTYPE_NAME: DOCTYPE_NAME,
297
- ERB_EXPR: ERB_EXPR,
298
- IS_ALLOWED_URI: IS_ALLOWED_URI,
299
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
300
- MUSTACHE_EXPR: MUSTACHE_EXPR,
301
- TMPLIT_EXPR: TMPLIT_EXPR
302
- });
303
-
304
333
  /* eslint-disable @typescript-eslint/indent */
305
334
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
306
335
  const NODE_TYPE = {
@@ -367,7 +396,7 @@ const _createHooksMap = function _createHooksMap() {
367
396
  function createDOMPurify() {
368
397
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
369
398
  const DOMPurify = root => createDOMPurify(root);
370
- DOMPurify.version = '3.4.2';
399
+ DOMPurify.version = '3.4.4';
371
400
  DOMPurify.removed = [];
372
401
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
373
402
  // Not running in a browser, provide a factory function
@@ -375,28 +404,26 @@ function createDOMPurify() {
375
404
  DOMPurify.isSupported = false;
376
405
  return DOMPurify;
377
406
  }
378
- let {
379
- document
380
- } = window;
407
+ let document = window.document;
381
408
  const originalDocument = document;
382
409
  const currentScript = originalDocument.currentScript;
383
- const {
384
- DocumentFragment,
385
- HTMLTemplateElement,
386
- Node,
387
- Element,
388
- NodeFilter,
389
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
390
- HTMLFormElement,
391
- DOMParser,
392
- trustedTypes
393
- } = window;
410
+ const DocumentFragment = window.DocumentFragment,
411
+ HTMLTemplateElement = window.HTMLTemplateElement,
412
+ Node = window.Node,
413
+ Element = window.Element,
414
+ NodeFilter = window.NodeFilter,
415
+ _window$NamedNodeMap = window.NamedNodeMap,
416
+ NamedNodeMap = _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
417
+ HTMLFormElement = window.HTMLFormElement,
418
+ DOMParser = window.DOMParser,
419
+ trustedTypes = window.trustedTypes;
394
420
  const ElementPrototype = Element.prototype;
395
421
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
396
422
  const remove = lookupGetter(ElementPrototype, 'remove');
397
423
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
398
424
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
399
425
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
426
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
400
427
  // As per issue #47, the web-components registry is inherited by a
401
428
  // new document created via createHTMLDocument. As per the spec
402
429
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -411,33 +438,26 @@ function createDOMPurify() {
411
438
  }
412
439
  let trustedTypesPolicy;
413
440
  let emptyHTML = '';
414
- const {
415
- implementation,
416
- createNodeIterator,
417
- createDocumentFragment,
418
- getElementsByTagName
419
- } = document;
420
- const {
421
- importNode
422
- } = originalDocument;
441
+ const _document = document,
442
+ implementation = _document.implementation,
443
+ createNodeIterator = _document.createNodeIterator,
444
+ createDocumentFragment = _document.createDocumentFragment,
445
+ getElementsByTagName = _document.getElementsByTagName;
446
+ const importNode = originalDocument.importNode;
423
447
  let hooks = _createHooksMap();
424
448
  /**
425
449
  * Expose whether this browser supports running the full DOMPurify.
426
450
  */
427
451
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
428
- const {
429
- MUSTACHE_EXPR,
430
- ERB_EXPR,
431
- TMPLIT_EXPR,
432
- DATA_ATTR,
433
- ARIA_ATTR,
434
- IS_SCRIPT_OR_DATA,
435
- ATTR_WHITESPACE,
436
- CUSTOM_ELEMENT
437
- } = EXPRESSIONS;
438
- let {
439
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
440
- } = EXPRESSIONS;
452
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
453
+ ERB_EXPR$1 = ERB_EXPR,
454
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
455
+ DATA_ATTR$1 = DATA_ATTR,
456
+ ARIA_ATTR$1 = ARIA_ATTR,
457
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
458
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
459
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
460
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
441
461
  /**
442
462
  * We consider the elements and attributes below to be safe. Ideally
443
463
  * don't add any new ones but feel free to remove unwanted ones.
@@ -957,6 +977,40 @@ function createDOMPurify() {
957
977
  // eslint-disable-next-line no-bitwise
958
978
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
959
979
  };
980
+ /**
981
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
982
+ * character data of an element subtree. Used as the final safety net for
983
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
984
+ * which only form after text-node normalization (e.g. fragments split across
985
+ * stripped elements) cannot survive into a template-evaluating framework.
986
+ *
987
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
988
+ * in place rather than round-tripping through innerHTML. This preserves
989
+ * descendant node references (important for IN_PLACE callers), avoids a
990
+ * serialize/reparse cycle, and reads literal character data — which means
991
+ * `<%...%>` in text content matches the ERB regex against its real bytes
992
+ * instead of the HTML-entity-escaped form innerHTML would produce.
993
+ *
994
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
995
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
996
+ *
997
+ * @param node The root element whose character data should be scrubbed.
998
+ */
999
+ const _scrubTemplateExpressions = function _scrubTemplateExpressions(node) {
1000
+ node.normalize();
1001
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
1002
+ // eslint-disable-next-line no-bitwise
1003
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1004
+ let currentNode = walker.nextNode();
1005
+ while (currentNode) {
1006
+ let data = currentNode.data;
1007
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1008
+ data = stringReplace(data, expr, ' ');
1009
+ });
1010
+ currentNode.data = data;
1011
+ currentNode = walker.nextNode();
1012
+ }
1013
+ };
960
1014
  /**
961
1015
  * _isClobbered
962
1016
  *
@@ -967,13 +1021,31 @@ function createDOMPurify() {
967
1021
  return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
968
1022
  };
969
1023
  /**
970
- * Checks whether the given object is a DOM node.
1024
+ * Checks whether the given object is a DOM node, including nodes that
1025
+ * originate from a different window/realm (e.g. an iframe's
1026
+ * contentDocument). The previous `value instanceof Node` check was
1027
+ * realm-bound: nodes from a different window failed it, causing
1028
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
1029
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
1030
+ *
1031
+ * Implementation: call the cached `nodeType` getter from Node.prototype
1032
+ * directly on the value. This bypasses any clobbered instance property
1033
+ * (e.g. a child element named "nodeType") and works across realms
1034
+ * because the WebIDL `nodeType` getter reads an internal slot that
1035
+ * every real Node has, regardless of which window minted it.
971
1036
  *
972
1037
  * @param value object to check whether it's a DOM node
973
- * @return true is object is a DOM node
1038
+ * @return true if value is a DOM node from any realm
974
1039
  */
975
1040
  const _isNode = function _isNode(value) {
976
- return typeof Node === 'function' && value instanceof Node;
1041
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1042
+ return false;
1043
+ }
1044
+ try {
1045
+ return typeof getNodeType(value) === 'number';
1046
+ } catch (_) {
1047
+ return false;
1048
+ }
977
1049
  };
978
1050
  function _executeHooks(hooks, currentNode, data) {
979
1051
  arrayForEach(hooks, hook => {
@@ -1065,7 +1137,7 @@ function createDOMPurify() {
1065
1137
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1066
1138
  /* Get the element's text content */
1067
1139
  content = currentNode.textContent;
1068
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1140
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1069
1141
  content = stringReplace(content, expr, ' ');
1070
1142
  });
1071
1143
  if (currentNode.textContent !== content) {
@@ -1102,7 +1174,7 @@ function createDOMPurify() {
1102
1174
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1103
1175
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1104
1176
  We don't need to check the value; it's always URI safe. */
1105
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1177
+ if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1106
1178
  if (
1107
1179
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1108
1180
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1114,7 +1186,7 @@ function createDOMPurify() {
1114
1186
  return false;
1115
1187
  }
1116
1188
  /* Check value is safe. First, is attr inert? If so, is safe */
1117
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
1189
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
1118
1190
  return false;
1119
1191
  } else ;
1120
1192
  return true;
@@ -1132,7 +1204,7 @@ function createDOMPurify() {
1132
1204
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1133
1205
  */
1134
1206
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1135
- return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT, tagName);
1207
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1136
1208
  };
1137
1209
  /**
1138
1210
  * _sanitizeAttributes
@@ -1147,9 +1219,7 @@ function createDOMPurify() {
1147
1219
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1148
1220
  /* Execute a hook if present */
1149
1221
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1150
- const {
1151
- attributes
1152
- } = currentNode;
1222
+ const attributes = currentNode.attributes;
1153
1223
  /* Check if we have attributes; if not we might have a text node */
1154
1224
  if (!attributes || _isClobbered(currentNode)) {
1155
1225
  return;
@@ -1165,11 +1235,9 @@ function createDOMPurify() {
1165
1235
  /* Go backwards over all attributes; safely remove bad ones */
1166
1236
  while (l--) {
1167
1237
  const attr = attributes[l];
1168
- const {
1169
- name,
1170
- namespaceURI,
1171
- value: attrValue
1172
- } = attr;
1238
+ const name = attr.name,
1239
+ namespaceURI = attr.namespaceURI,
1240
+ attrValue = attr.value;
1173
1241
  const lcName = transformCaseFunc(name);
1174
1242
  const initValue = attrValue;
1175
1243
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1217,7 +1285,7 @@ function createDOMPurify() {
1217
1285
  }
1218
1286
  /* Sanitize attribute content to be template-safe */
1219
1287
  if (SAFE_FOR_TEMPLATES) {
1220
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1288
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1221
1289
  value = stringReplace(value, expr, ' ');
1222
1290
  });
1223
1291
  }
@@ -1291,6 +1359,49 @@ function createDOMPurify() {
1291
1359
  /* Execute a hook if present */
1292
1360
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1293
1361
  };
1362
+ /**
1363
+ * _sanitizeAttachedShadowRoots
1364
+ *
1365
+ * Walks `root` and feeds every attached shadow root we encounter into
1366
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
1367
+ * does not descend into shadow trees, so nodes inside an attached
1368
+ * shadow root would otherwise be skipped entirely.
1369
+ *
1370
+ * Two real input paths put attached shadow roots in front of us:
1371
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
1372
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
1373
+ * shadow root because it was created with `clonable: true`.
1374
+ *
1375
+ * This pass runs once, up front, so the main iteration loop (and the
1376
+ * existing _sanitizeShadowDOM template-content recursion) stay
1377
+ * untouched — string-input paths are not affected.
1378
+ *
1379
+ * @param root the subtree root to walk for attached shadow roots
1380
+ */
1381
+ const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1382
+ if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
1383
+ const sr = root.shadowRoot;
1384
+ // Recurse first so that nested shadow roots are reached even if
1385
+ // _sanitizeShadowDOM removes hosts at this level.
1386
+ _sanitizeAttachedShadowRoots2(sr);
1387
+ _sanitizeShadowDOM2(sr);
1388
+ }
1389
+ // Snapshot children before recursing. Sanitization of one subtree
1390
+ // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1391
+ // and naive nextSibling traversal would silently skip the rest of
1392
+ // the list once a node is detached.
1393
+ const childNodes = root.childNodes;
1394
+ if (!childNodes) {
1395
+ return;
1396
+ }
1397
+ const snapshot = [];
1398
+ arrayForEach(childNodes, child => {
1399
+ arrayPush(snapshot, child);
1400
+ });
1401
+ for (const child of snapshot) {
1402
+ _sanitizeAttachedShadowRoots2(child);
1403
+ }
1404
+ };
1294
1405
  // eslint-disable-next-line complexity
1295
1406
  DOMPurify.sanitize = function (dirty) {
1296
1407
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1335,7 +1446,10 @@ function createDOMPurify() {
1335
1446
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1336
1447
  }
1337
1448
  }
1338
- } else if (dirty instanceof Node) {
1449
+ /* Sanitize attached shadow roots before the main iterator runs.
1450
+ The iterator does not descend into shadow trees. */
1451
+ _sanitizeAttachedShadowRoots2(dirty);
1452
+ } else if (_isNode(dirty)) {
1339
1453
  /* If dirty is a DOM element, append to an empty document to avoid
1340
1454
  elements being stripped by the parser */
1341
1455
  body = _initDocument('<!---->');
@@ -1349,6 +1463,10 @@ function createDOMPurify() {
1349
1463
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1350
1464
  body.appendChild(importedNode);
1351
1465
  }
1466
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1467
+ them before the main iterator runs, since the iterator does not
1468
+ descend into shadow trees. */
1469
+ _sanitizeAttachedShadowRoots2(importedNode);
1352
1470
  } else {
1353
1471
  /* Exit directly if we have nothing to do */
1354
1472
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
@@ -1382,17 +1500,15 @@ function createDOMPurify() {
1382
1500
  }
1383
1501
  /* If we sanitized `dirty` in-place, return it. */
1384
1502
  if (IN_PLACE) {
1503
+ if (SAFE_FOR_TEMPLATES) {
1504
+ _scrubTemplateExpressions(dirty);
1505
+ }
1385
1506
  return dirty;
1386
1507
  }
1387
1508
  /* Return sanitized string or DOM */
1388
1509
  if (RETURN_DOM) {
1389
1510
  if (SAFE_FOR_TEMPLATES) {
1390
- body.normalize();
1391
- let html = body.innerHTML;
1392
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1393
- html = stringReplace(html, expr, ' ');
1394
- });
1395
- body.innerHTML = html;
1511
+ _scrubTemplateExpressions(body);
1396
1512
  }
1397
1513
  if (RETURN_DOM_FRAGMENT) {
1398
1514
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1422,7 +1538,7 @@ function createDOMPurify() {
1422
1538
  }
1423
1539
  /* Sanitize final string template-safe */
1424
1540
  if (SAFE_FOR_TEMPLATES) {
1425
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1541
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1426
1542
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1427
1543
  });
1428
1544
  }