dompurify 3.4.1 → 3.4.3

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
@@ -6,14 +6,13 @@
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.1**.
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.3**.
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
-
16
- 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.
17
16
 
18
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.
19
18
 
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.1 | (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.1/LICENSE */
1
+ /*! @license DOMPurify 3.4.3 | (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.3/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.1 | (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.1/LICENSE */
1
+ /*! @license DOMPurify 3.4.3 | (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.3/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)) {
@@ -273,10 +317,9 @@ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseli
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.1';
399
+ DOMPurify.version = '3.4.3';
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,22 +404,19 @@ 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');
@@ -411,33 +437,26 @@ function createDOMPurify() {
411
437
  }
412
438
  let trustedTypesPolicy;
413
439
  let emptyHTML = '';
414
- const {
415
- implementation,
416
- createNodeIterator,
417
- createDocumentFragment,
418
- getElementsByTagName
419
- } = document;
420
- const {
421
- importNode
422
- } = originalDocument;
440
+ const _document = document,
441
+ implementation = _document.implementation,
442
+ createNodeIterator = _document.createNodeIterator,
443
+ createDocumentFragment = _document.createDocumentFragment,
444
+ getElementsByTagName = _document.getElementsByTagName;
445
+ const importNode = originalDocument.importNode;
423
446
  let hooks = _createHooksMap();
424
447
  /**
425
448
  * Expose whether this browser supports running the full DOMPurify.
426
449
  */
427
450
  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;
451
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
452
+ ERB_EXPR$1 = ERB_EXPR,
453
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
454
+ DATA_ATTR$1 = DATA_ATTR,
455
+ ARIA_ATTR$1 = ARIA_ATTR,
456
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
457
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
458
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
459
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
441
460
  /**
442
461
  * We consider the elements and attributes below to be safe. Ideally
443
462
  * don't add any new ones but feel free to remove unwanted ones.
@@ -1065,7 +1084,7 @@ function createDOMPurify() {
1065
1084
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1066
1085
  /* Get the element's text content */
1067
1086
  content = currentNode.textContent;
1068
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1087
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1069
1088
  content = stringReplace(content, expr, ' ');
1070
1089
  });
1071
1090
  if (currentNode.textContent !== content) {
@@ -1097,11 +1116,12 @@ function createDOMPurify() {
1097
1116
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1098
1117
  return false;
1099
1118
  }
1119
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
1100
1120
  /* Allow valid data-* attributes: At least one character after "-"
1101
1121
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1102
1122
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1103
1123
  We don't need to check the value; it's always URI safe. */
1104
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
1124
+ 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]) {
1105
1125
  if (
1106
1126
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1107
1127
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1113,7 +1133,7 @@ function createDOMPurify() {
1113
1133
  return false;
1114
1134
  }
1115
1135
  /* Check value is safe. First, is attr inert? If so, is safe */
1116
- } 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) {
1136
+ } 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) {
1117
1137
  return false;
1118
1138
  } else ;
1119
1139
  return true;
@@ -1131,7 +1151,7 @@ function createDOMPurify() {
1131
1151
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1132
1152
  */
1133
1153
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1134
- return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT, tagName);
1154
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1135
1155
  };
1136
1156
  /**
1137
1157
  * _sanitizeAttributes
@@ -1146,9 +1166,7 @@ function createDOMPurify() {
1146
1166
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1147
1167
  /* Execute a hook if present */
1148
1168
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1149
- const {
1150
- attributes
1151
- } = currentNode;
1169
+ const attributes = currentNode.attributes;
1152
1170
  /* Check if we have attributes; if not we might have a text node */
1153
1171
  if (!attributes || _isClobbered(currentNode)) {
1154
1172
  return;
@@ -1164,11 +1182,9 @@ function createDOMPurify() {
1164
1182
  /* Go backwards over all attributes; safely remove bad ones */
1165
1183
  while (l--) {
1166
1184
  const attr = attributes[l];
1167
- const {
1168
- name,
1169
- namespaceURI,
1170
- value: attrValue
1171
- } = attr;
1185
+ const name = attr.name,
1186
+ namespaceURI = attr.namespaceURI,
1187
+ attrValue = attr.value;
1172
1188
  const lcName = transformCaseFunc(name);
1173
1189
  const initValue = attrValue;
1174
1190
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1216,7 +1232,7 @@ function createDOMPurify() {
1216
1232
  }
1217
1233
  /* Sanitize attribute content to be template-safe */
1218
1234
  if (SAFE_FOR_TEMPLATES) {
1219
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1235
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1220
1236
  value = stringReplace(value, expr, ' ');
1221
1237
  });
1222
1238
  }
@@ -1290,6 +1306,49 @@ function createDOMPurify() {
1290
1306
  /* Execute a hook if present */
1291
1307
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1292
1308
  };
1309
+ /**
1310
+ * _sanitizeAttachedShadowRoots
1311
+ *
1312
+ * Walks `root` and feeds every attached shadow root we encounter into
1313
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
1314
+ * does not descend into shadow trees, so nodes inside an attached
1315
+ * shadow root would otherwise be skipped entirely.
1316
+ *
1317
+ * Two real input paths put attached shadow roots in front of us:
1318
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
1319
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
1320
+ * shadow root because it was created with `clonable: true`.
1321
+ *
1322
+ * This pass runs once, up front, so the main iteration loop (and the
1323
+ * existing _sanitizeShadowDOM template-content recursion) stay
1324
+ * untouched — string-input paths are not affected.
1325
+ *
1326
+ * @param root the subtree root to walk for attached shadow roots
1327
+ */
1328
+ const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1329
+ if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
1330
+ const sr = root.shadowRoot;
1331
+ // Recurse first so that nested shadow roots are reached even if
1332
+ // _sanitizeShadowDOM removes hosts at this level.
1333
+ _sanitizeAttachedShadowRoots2(sr);
1334
+ _sanitizeShadowDOM2(sr);
1335
+ }
1336
+ // Snapshot children before recursing. Sanitization of one subtree
1337
+ // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1338
+ // and naive nextSibling traversal would silently skip the rest of
1339
+ // the list once a node is detached.
1340
+ const childNodes = root.childNodes;
1341
+ if (!childNodes) {
1342
+ return;
1343
+ }
1344
+ const snapshot = [];
1345
+ arrayForEach(childNodes, child => {
1346
+ arrayPush(snapshot, child);
1347
+ });
1348
+ for (const child of snapshot) {
1349
+ _sanitizeAttachedShadowRoots2(child);
1350
+ }
1351
+ };
1293
1352
  // eslint-disable-next-line complexity
1294
1353
  DOMPurify.sanitize = function (dirty) {
1295
1354
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1334,6 +1393,9 @@ function createDOMPurify() {
1334
1393
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1335
1394
  }
1336
1395
  }
1396
+ /* Sanitize attached shadow roots before the main iterator runs.
1397
+ The iterator does not descend into shadow trees. */
1398
+ _sanitizeAttachedShadowRoots2(dirty);
1337
1399
  } else if (dirty instanceof Node) {
1338
1400
  /* If dirty is a DOM element, append to an empty document to avoid
1339
1401
  elements being stripped by the parser */
@@ -1348,6 +1410,10 @@ function createDOMPurify() {
1348
1410
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1349
1411
  body.appendChild(importedNode);
1350
1412
  }
1413
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1414
+ them before the main iterator runs, since the iterator does not
1415
+ descend into shadow trees. */
1416
+ _sanitizeAttachedShadowRoots2(importedNode);
1351
1417
  } else {
1352
1418
  /* Exit directly if we have nothing to do */
1353
1419
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
@@ -1388,7 +1454,7 @@ function createDOMPurify() {
1388
1454
  if (SAFE_FOR_TEMPLATES) {
1389
1455
  body.normalize();
1390
1456
  let html = body.innerHTML;
1391
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1457
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1392
1458
  html = stringReplace(html, expr, ' ');
1393
1459
  });
1394
1460
  body.innerHTML = html;
@@ -1421,7 +1487,7 @@ function createDOMPurify() {
1421
1487
  }
1422
1488
  /* Sanitize final string template-safe */
1423
1489
  if (SAFE_FOR_TEMPLATES) {
1424
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1490
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1425
1491
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1426
1492
  });
1427
1493
  }