dompurify 3.4.2 → 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,13 +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.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.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
- 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
 
@@ -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.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.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.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.2';
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) {
@@ -1102,7 +1121,7 @@ function createDOMPurify() {
1102
1121
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1103
1122
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1104
1123
  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]) {
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]) {
1106
1125
  if (
1107
1126
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1108
1127
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1114,7 +1133,7 @@ function createDOMPurify() {
1114
1133
  return false;
1115
1134
  }
1116
1135
  /* 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) {
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) {
1118
1137
  return false;
1119
1138
  } else ;
1120
1139
  return true;
@@ -1132,7 +1151,7 @@ function createDOMPurify() {
1132
1151
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1133
1152
  */
1134
1153
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1135
- return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT, tagName);
1154
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1136
1155
  };
1137
1156
  /**
1138
1157
  * _sanitizeAttributes
@@ -1147,9 +1166,7 @@ function createDOMPurify() {
1147
1166
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1148
1167
  /* Execute a hook if present */
1149
1168
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1150
- const {
1151
- attributes
1152
- } = currentNode;
1169
+ const attributes = currentNode.attributes;
1153
1170
  /* Check if we have attributes; if not we might have a text node */
1154
1171
  if (!attributes || _isClobbered(currentNode)) {
1155
1172
  return;
@@ -1165,11 +1182,9 @@ function createDOMPurify() {
1165
1182
  /* Go backwards over all attributes; safely remove bad ones */
1166
1183
  while (l--) {
1167
1184
  const attr = attributes[l];
1168
- const {
1169
- name,
1170
- namespaceURI,
1171
- value: attrValue
1172
- } = attr;
1185
+ const name = attr.name,
1186
+ namespaceURI = attr.namespaceURI,
1187
+ attrValue = attr.value;
1173
1188
  const lcName = transformCaseFunc(name);
1174
1189
  const initValue = attrValue;
1175
1190
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1217,7 +1232,7 @@ function createDOMPurify() {
1217
1232
  }
1218
1233
  /* Sanitize attribute content to be template-safe */
1219
1234
  if (SAFE_FOR_TEMPLATES) {
1220
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1235
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1221
1236
  value = stringReplace(value, expr, ' ');
1222
1237
  });
1223
1238
  }
@@ -1291,6 +1306,49 @@ function createDOMPurify() {
1291
1306
  /* Execute a hook if present */
1292
1307
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1293
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
+ };
1294
1352
  // eslint-disable-next-line complexity
1295
1353
  DOMPurify.sanitize = function (dirty) {
1296
1354
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1335,6 +1393,9 @@ function createDOMPurify() {
1335
1393
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1336
1394
  }
1337
1395
  }
1396
+ /* Sanitize attached shadow roots before the main iterator runs.
1397
+ The iterator does not descend into shadow trees. */
1398
+ _sanitizeAttachedShadowRoots2(dirty);
1338
1399
  } else if (dirty instanceof Node) {
1339
1400
  /* If dirty is a DOM element, append to an empty document to avoid
1340
1401
  elements being stripped by the parser */
@@ -1349,6 +1410,10 @@ function createDOMPurify() {
1349
1410
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1350
1411
  body.appendChild(importedNode);
1351
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);
1352
1417
  } else {
1353
1418
  /* Exit directly if we have nothing to do */
1354
1419
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
@@ -1389,7 +1454,7 @@ function createDOMPurify() {
1389
1454
  if (SAFE_FOR_TEMPLATES) {
1390
1455
  body.normalize();
1391
1456
  let html = body.innerHTML;
1392
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1457
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1393
1458
  html = stringReplace(html, expr, ' ');
1394
1459
  });
1395
1460
  body.innerHTML = html;
@@ -1422,7 +1487,7 @@ function createDOMPurify() {
1422
1487
  }
1423
1488
  /* Sanitize final string template-safe */
1424
1489
  if (SAFE_FOR_TEMPLATES) {
1425
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1490
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1426
1491
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1427
1492
  });
1428
1493
  }