@vaadin/bundles 24.10.3 → 24.10.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.
@@ -6,29 +6,70 @@
6
6
  \***********************************************/
7
7
  /***/ (function(module) {
8
8
 
9
- /*! @license DOMPurify 3.4.0 | (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.0/LICENSE */
9
+ /*! @license DOMPurify 3.4.8 | (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.8/LICENSE */
10
10
 
11
11
  (function (global, factory) {
12
12
  true ? module.exports = factory() :
13
13
  0;
14
14
  })(this, (function () { 'use strict';
15
15
 
16
- const {
17
- entries,
18
- setPrototypeOf,
19
- isFrozen,
20
- getPrototypeOf,
21
- getOwnPropertyDescriptor
22
- } = Object;
23
- let {
24
- freeze,
25
- seal,
26
- create
27
- } = Object; // eslint-disable-line import/no-mutable-exports
28
- let {
29
- apply,
30
- construct
31
- } = typeof Reflect !== 'undefined' && Reflect;
16
+ function _arrayLikeToArray(r, a) {
17
+ (null == a || a > r.length) && (a = r.length);
18
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
19
+ return n;
20
+ }
21
+ function _arrayWithHoles(r) {
22
+ if (Array.isArray(r)) return r;
23
+ }
24
+ function _iterableToArrayLimit(r, l) {
25
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
26
+ if (null != t) {
27
+ var e,
28
+ n,
29
+ i,
30
+ u,
31
+ a = [],
32
+ f = true,
33
+ o = false;
34
+ try {
35
+ 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);
36
+ } catch (r) {
37
+ o = true, n = r;
38
+ } finally {
39
+ try {
40
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
41
+ } finally {
42
+ if (o) throw n;
43
+ }
44
+ }
45
+ return a;
46
+ }
47
+ }
48
+ function _nonIterableRest() {
49
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
50
+ }
51
+ function _slicedToArray(r, e) {
52
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
53
+ }
54
+ function _unsupportedIterableToArray(r, a) {
55
+ if (r) {
56
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
57
+ var t = {}.toString.call(r).slice(8, -1);
58
+ 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;
59
+ }
60
+ }
61
+
62
+ const entries = Object.entries,
63
+ setPrototypeOf = Object.setPrototypeOf,
64
+ isFrozen = Object.isFrozen,
65
+ getPrototypeOf = Object.getPrototypeOf,
66
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
67
+ let freeze = Object.freeze,
68
+ seal = Object.seal,
69
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
70
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
71
+ apply = _ref.apply,
72
+ construct = _ref.construct;
32
73
  if (!freeze) {
33
74
  freeze = function freeze(x) {
34
75
  return x;
@@ -60,13 +101,19 @@
60
101
  const arrayPop = unapply(Array.prototype.pop);
61
102
  const arrayPush = unapply(Array.prototype.push);
62
103
  const arraySplice = unapply(Array.prototype.splice);
104
+ const arrayIsArray = Array.isArray;
63
105
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
64
106
  const stringToString = unapply(String.prototype.toString);
65
107
  const stringMatch = unapply(String.prototype.match);
66
108
  const stringReplace = unapply(String.prototype.replace);
67
109
  const stringIndexOf = unapply(String.prototype.indexOf);
68
110
  const stringTrim = unapply(String.prototype.trim);
111
+ const numberToString = unapply(Number.prototype.toString);
112
+ const booleanToString = unapply(Boolean.prototype.toString);
113
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
114
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
69
115
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
116
+ const objectToString = unapply(Object.prototype.toString);
70
117
  const regExpTest = unapply(RegExp.prototype.test);
71
118
  const typeErrorCreate = unconstruct(TypeError);
72
119
  /**
@@ -116,6 +163,9 @@
116
163
  // Prevent prototype setters from intercepting set as a this value.
117
164
  setPrototypeOf(set, null);
118
165
  }
166
+ if (!arrayIsArray(array)) {
167
+ return set;
168
+ }
119
169
  let l = array.length;
120
170
  while (l--) {
121
171
  let element = array[l];
@@ -156,10 +206,13 @@
156
206
  */
157
207
  function clone(object) {
158
208
  const newObject = create(null);
159
- for (const [property, value] of entries(object)) {
209
+ for (const _ref2 of entries(object)) {
210
+ var _ref3 = _slicedToArray(_ref2, 2);
211
+ const property = _ref3[0];
212
+ const value = _ref3[1];
160
213
  const isPropertyExist = objectHasOwnProperty(object, property);
161
214
  if (isPropertyExist) {
162
- if (Array.isArray(value)) {
215
+ if (arrayIsArray(value)) {
163
216
  newObject[property] = cleanArray(value);
164
217
  } else if (value && typeof value === 'object' && value.constructor === Object) {
165
218
  newObject[property] = clone(value);
@@ -170,6 +223,58 @@
170
223
  }
171
224
  return newObject;
172
225
  }
226
+ /**
227
+ * Convert non-node values into strings without depending on direct property access.
228
+ *
229
+ * @param value - The value to stringify.
230
+ * @returns A string representation of the provided value.
231
+ */
232
+ function stringifyValue(value) {
233
+ switch (typeof value) {
234
+ case 'string':
235
+ {
236
+ return value;
237
+ }
238
+ case 'number':
239
+ {
240
+ return numberToString(value);
241
+ }
242
+ case 'boolean':
243
+ {
244
+ return booleanToString(value);
245
+ }
246
+ case 'bigint':
247
+ {
248
+ return bigintToString ? bigintToString(value) : '0';
249
+ }
250
+ case 'symbol':
251
+ {
252
+ return symbolToString ? symbolToString(value) : 'Symbol()';
253
+ }
254
+ case 'undefined':
255
+ {
256
+ return objectToString(value);
257
+ }
258
+ case 'function':
259
+ case 'object':
260
+ {
261
+ if (value === null) {
262
+ return objectToString(value);
263
+ }
264
+ const valueAsRecord = value;
265
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
266
+ if (typeof valueToString === 'function') {
267
+ const stringified = valueToString(valueAsRecord);
268
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
269
+ }
270
+ return objectToString(value);
271
+ }
272
+ default:
273
+ {
274
+ return objectToString(value);
275
+ }
276
+ }
277
+ }
173
278
  /**
174
279
  * This method automatically checks if the prop is function or getter and behaves accordingly.
175
280
  *
@@ -195,6 +300,14 @@
195
300
  }
196
301
  return fallbackValue;
197
302
  }
303
+ function isRegex(value) {
304
+ try {
305
+ regExpTest(value, '');
306
+ return true;
307
+ } catch (_unused) {
308
+ return false;
309
+ }
310
+ }
198
311
 
199
312
  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']);
200
313
  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']);
@@ -210,15 +323,14 @@
210
323
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
211
324
  const text = freeze(['#text']);
212
325
 
213
- 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', 'slot']);
326
+ 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']);
214
327
  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']);
215
328
  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']);
216
329
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
217
330
 
218
- // eslint-disable-next-line unicorn/better-regex
219
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
220
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
221
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
331
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
332
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
333
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
222
334
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
223
335
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
224
336
  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
@@ -229,29 +341,24 @@
229
341
  const DOCTYPE_NAME = seal(/^html$/i);
230
342
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
231
343
 
232
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
233
- __proto__: null,
234
- ARIA_ATTR: ARIA_ATTR,
235
- ATTR_WHITESPACE: ATTR_WHITESPACE,
236
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
237
- DATA_ATTR: DATA_ATTR,
238
- DOCTYPE_NAME: DOCTYPE_NAME,
239
- ERB_EXPR: ERB_EXPR,
240
- IS_ALLOWED_URI: IS_ALLOWED_URI,
241
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
242
- MUSTACHE_EXPR: MUSTACHE_EXPR,
243
- TMPLIT_EXPR: TMPLIT_EXPR
244
- });
245
-
246
344
  /* eslint-disable @typescript-eslint/indent */
247
345
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
248
346
  const NODE_TYPE = {
249
347
  element: 1,
348
+ attribute: 2,
250
349
  text: 3,
350
+ cdataSection: 4,
351
+ entityReference: 5,
352
+ // Deprecated
353
+ entityNode: 6,
251
354
  // Deprecated
252
355
  progressingInstruction: 7,
253
356
  comment: 8,
254
- document: 9};
357
+ document: 9,
358
+ documentType: 10,
359
+ documentFragment: 11,
360
+ notation: 12 // Deprecated
361
+ };
255
362
  const getGlobal = function getGlobal() {
256
363
  return typeof window === 'undefined' ? null : window;
257
364
  };
@@ -309,7 +416,7 @@
309
416
  function createDOMPurify() {
310
417
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
311
418
  const DOMPurify = root => createDOMPurify(root);
312
- DOMPurify.version = '3.4.0';
419
+ DOMPurify.version = '3.4.8';
313
420
  DOMPurify.removed = [];
314
421
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
315
422
  // Not running in a browser, provide a factory function
@@ -317,28 +424,29 @@
317
424
  DOMPurify.isSupported = false;
318
425
  return DOMPurify;
319
426
  }
320
- let {
321
- document
322
- } = window;
427
+ let document = window.document;
323
428
  const originalDocument = document;
324
429
  const currentScript = originalDocument.currentScript;
325
- const {
326
- DocumentFragment,
327
- HTMLTemplateElement,
328
- Node,
329
- Element,
330
- NodeFilter,
331
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
332
- HTMLFormElement,
333
- DOMParser,
334
- trustedTypes
335
- } = window;
430
+ window.DocumentFragment;
431
+ const HTMLTemplateElement = window.HTMLTemplateElement,
432
+ Node = window.Node,
433
+ Element = window.Element,
434
+ NodeFilter = window.NodeFilter,
435
+ _window$NamedNodeMap = window.NamedNodeMap;
436
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
437
+ window.HTMLFormElement;
438
+ const DOMParser = window.DOMParser,
439
+ trustedTypes = window.trustedTypes;
336
440
  const ElementPrototype = Element.prototype;
337
441
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
338
442
  const remove = lookupGetter(ElementPrototype, 'remove');
339
443
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
340
444
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
341
445
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
446
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
447
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
448
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
449
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
342
450
  // As per issue #47, the web-components registry is inherited by a
343
451
  // new document created via createHTMLDocument. As per the spec
344
452
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -353,33 +461,43 @@
353
461
  }
354
462
  let trustedTypesPolicy;
355
463
  let emptyHTML = '';
356
- const {
357
- implementation,
358
- createNodeIterator,
359
- createDocumentFragment,
360
- getElementsByTagName
361
- } = document;
362
- const {
363
- importNode
364
- } = originalDocument;
464
+ // Tracks whether we are already inside a call to the configured Trusted Types
465
+ // policy's `createHTML`. If the supplied `TRUSTED_TYPES_POLICY.createHTML`
466
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
467
+ // re-enter the policy and recurse until the stack overflows. We detect that
468
+ // re-entry and throw a clear, actionable error instead.
469
+ let IN_POLICY_CREATE_HTML = 0;
470
+ const _createTrustedHTML = function _createTrustedHTML(html) {
471
+ if (IN_POLICY_CREATE_HTML > 0) {
472
+ throw typeErrorCreate('The configured TRUSTED_TYPES_POLICY.createHTML must not call ' + 'DOMPurify.sanitize, as that causes infinite recursion. Do not pass ' + 'a policy whose createHTML wraps DOMPurify as TRUSTED_TYPES_POLICY; ' + 'see the "DOMPurify and Trusted Types" section of the README.');
473
+ }
474
+ IN_POLICY_CREATE_HTML++;
475
+ try {
476
+ return trustedTypesPolicy.createHTML(html);
477
+ } finally {
478
+ IN_POLICY_CREATE_HTML--;
479
+ }
480
+ };
481
+ const _document = document,
482
+ implementation = _document.implementation,
483
+ createNodeIterator = _document.createNodeIterator,
484
+ createDocumentFragment = _document.createDocumentFragment,
485
+ getElementsByTagName = _document.getElementsByTagName;
486
+ const importNode = originalDocument.importNode;
365
487
  let hooks = _createHooksMap();
366
488
  /**
367
489
  * Expose whether this browser supports running the full DOMPurify.
368
490
  */
369
491
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
370
- const {
371
- MUSTACHE_EXPR,
372
- ERB_EXPR,
373
- TMPLIT_EXPR,
374
- DATA_ATTR,
375
- ARIA_ATTR,
376
- IS_SCRIPT_OR_DATA,
377
- ATTR_WHITESPACE,
378
- CUSTOM_ELEMENT
379
- } = EXPRESSIONS;
380
- let {
381
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
382
- } = EXPRESSIONS;
492
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
493
+ ERB_EXPR$1 = ERB_EXPR,
494
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
495
+ DATA_ATTR$1 = DATA_ATTR,
496
+ ARIA_ATTR$1 = ARIA_ATTR,
497
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
498
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
499
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
500
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
383
501
  /**
384
502
  * We consider the elements and attributes below to be safe. Ideally
385
503
  * don't add any new ones but feel free to remove unwanted ones.
@@ -557,15 +675,15 @@
557
675
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
558
676
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
559
677
  /* Set configuration parameters */
560
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
561
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
562
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
563
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
564
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
565
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
566
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
567
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
568
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
678
+ ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
679
+ ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') && arrayIsArray(cfg.ALLOWED_ATTR) ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
680
+ ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') && arrayIsArray(cfg.ALLOWED_NAMESPACES) ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
681
+ URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR) ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
682
+ DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') && arrayIsArray(cfg.ADD_DATA_URI_TAGS) ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
683
+ FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS) ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
684
+ FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') && arrayIsArray(cfg.FORBID_TAGS) ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
685
+ FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR) ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
686
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
569
687
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
570
688
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
571
689
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -581,19 +699,20 @@
581
699
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
582
700
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
583
701
  IN_PLACE = cfg.IN_PLACE || false; // Default false
584
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
585
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
586
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
587
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
588
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
589
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
590
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
702
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
703
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
704
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); // Default built-in map
705
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, ['annotation-xml']); // Default built-in map
706
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
707
+ CUSTOM_ELEMENT_HANDLING = create(null);
708
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
709
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
591
710
  }
592
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
593
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
711
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
712
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
594
713
  }
595
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
596
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
714
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
715
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
597
716
  }
598
717
  if (SAFE_FOR_TEMPLATES) {
599
718
  ALLOW_DATA_ATTR = false;
@@ -630,36 +749,36 @@
630
749
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
631
750
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
632
751
  /* Merge configuration parameters */
633
- if (cfg.ADD_TAGS) {
752
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
634
753
  if (typeof cfg.ADD_TAGS === 'function') {
635
754
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
636
- } else {
755
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
637
756
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
638
757
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
639
758
  }
640
759
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
641
760
  }
642
761
  }
643
- if (cfg.ADD_ATTR) {
762
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
644
763
  if (typeof cfg.ADD_ATTR === 'function') {
645
764
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
646
- } else {
765
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
647
766
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
648
767
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
649
768
  }
650
769
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
651
770
  }
652
771
  }
653
- if (cfg.ADD_URI_SAFE_ATTR) {
772
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
654
773
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
655
774
  }
656
- if (cfg.FORBID_CONTENTS) {
775
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
657
776
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
658
777
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
659
778
  }
660
779
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
661
780
  }
662
- if (cfg.ADD_FORBID_CONTENTS) {
781
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
663
782
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
664
783
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
665
784
  }
@@ -686,19 +805,47 @@
686
805
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
687
806
  }
688
807
  // Overwrite existing TrustedTypes policy.
808
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
689
809
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
690
- // Sign local variables required by `sanitize`.
691
- emptyHTML = trustedTypesPolicy.createHTML('');
810
+ // Sign local variables required by `sanitize`. If the supplied policy's
811
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
812
+ // throws via the re-entrancy guard. Restore the previous policy first so
813
+ // the instance is not left in a poisoned state. See #1422.
814
+ try {
815
+ emptyHTML = _createTrustedHTML('');
816
+ } catch (error) {
817
+ trustedTypesPolicy = previousTrustedTypesPolicy;
818
+ throw error;
819
+ }
692
820
  } else {
693
821
  // Uninitialized policy, attempt to initialize the internal dompurify policy.
694
- if (trustedTypesPolicy === undefined) {
822
+ if (trustedTypesPolicy === undefined && cfg.TRUSTED_TYPES_POLICY !== null) {
695
823
  trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
696
824
  }
697
825
  // If creating the internal policy succeeded sign internal variables.
698
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
699
- emptyHTML = trustedTypesPolicy.createHTML('');
826
+ // Note: a falsy `trustedTypesPolicy` (null when policy creation failed or
827
+ // was skipped via `TRUSTED_TYPES_POLICY: null`, or undefined when no
828
+ // policy has been initialized yet) must be excluded here, otherwise we
829
+ // would call `.createHTML` on a non-policy and throw. See #1422.
830
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
831
+ emptyHTML = _createTrustedHTML('');
700
832
  }
701
833
  }
834
+ /*
835
+ * Mirror the clone-before-mutate pattern already applied above for
836
+ * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
837
+ * registered AND the set still points at the default constant,
838
+ * clone it. The hook then mutates the clone (in-call widening
839
+ * still works exactly as documented) and the next default-cfg
840
+ * call rebinds to the untouched original via the reassignment at
841
+ * the top of this function.
842
+ */
843
+ if ((hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) && ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
844
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
845
+ }
846
+ if (hooks.uponSanitizeAttribute.length > 0 && ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
847
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
848
+ }
702
849
  // Prevent further manipulation of configuration.
703
850
  // Not available in IE8, Safari 5, etc.
704
851
  if (freeze) {
@@ -858,7 +1005,7 @@
858
1005
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
859
1006
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
860
1007
  }
861
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
1008
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
862
1009
  /*
863
1010
  * Use the DOMParser API by default, fallback later if needs be
864
1011
  * DOMParser not work for svg when has multiple root element.
@@ -898,23 +1045,142 @@
898
1045
  // eslint-disable-next-line no-bitwise
899
1046
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
900
1047
  };
1048
+ /**
1049
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1050
+ * character data of an element subtree. Used as the final safety net for
1051
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
1052
+ * which only form after text-node normalization (e.g. fragments split across
1053
+ * stripped elements) cannot survive into a template-evaluating framework.
1054
+ *
1055
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
1056
+ * in place rather than round-tripping through innerHTML. This preserves
1057
+ * descendant node references (important for IN_PLACE callers), avoids a
1058
+ * serialize/reparse cycle, and reads literal character data — which means
1059
+ * `<%...%>` in text content matches the ERB regex against its real bytes
1060
+ * instead of the HTML-entity-escaped form innerHTML would produce.
1061
+ *
1062
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
1063
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
1064
+ *
1065
+ * @param node The root element whose character data should be scrubbed.
1066
+ */
1067
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1068
+ var _node$querySelectorAl, _node$querySelectorAl2;
1069
+ node.normalize();
1070
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
1071
+ // eslint-disable-next-line no-bitwise
1072
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1073
+ let currentNode = walker.nextNode();
1074
+ while (currentNode) {
1075
+ let data = currentNode.data;
1076
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1077
+ data = stringReplace(data, expr, ' ');
1078
+ });
1079
+ currentNode.data = data;
1080
+ currentNode = walker.nextNode();
1081
+ }
1082
+ // NodeIterator does not descend into <template>.content per the DOM spec,
1083
+ // so we must explicitly recurse into each template's content fragment,
1084
+ // mirroring the approach used by _sanitizeShadowDOM.
1085
+ const templates = (_node$querySelectorAl = (_node$querySelectorAl2 = node.querySelectorAll) === null || _node$querySelectorAl2 === void 0 ? void 0 : _node$querySelectorAl2.call(node, 'template')) !== null && _node$querySelectorAl !== void 0 ? _node$querySelectorAl : [];
1086
+ arrayForEach(Array.from(templates), tmpl => {
1087
+ if (_isDocumentFragment(tmpl.content)) {
1088
+ _scrubTemplateExpressions2(tmpl.content);
1089
+ }
1090
+ });
1091
+ };
901
1092
  /**
902
1093
  * _isClobbered
903
1094
  *
1095
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
1096
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
1097
+ * `name` attribute matching a prototype property shadows that property
1098
+ * on direct reads. We use this check at the IN_PLACE entry-point and
1099
+ * during attribute sanitization to refuse clobbered forms.
1100
+ *
904
1101
  * @param element element to check for clobbering attacks
905
1102
  * @return true if clobbered, false if safe
906
1103
  */
907
1104
  const _isClobbered = function _isClobbered(element) {
908
- 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');
1105
+ // Realm-independent tag-name probe. If we can't determine the tag
1106
+ // name at all, we can't reason about clobbering — return false
1107
+ // (the caller's other defences still apply).
1108
+ const realTagName = getNodeName ? getNodeName(element) : null;
1109
+ if (typeof realTagName !== 'string') {
1110
+ return false;
1111
+ }
1112
+ if (transformCaseFunc(realTagName) !== 'form') {
1113
+ return false;
1114
+ }
1115
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
1116
+ // Realm-safe NamedNodeMap detection: equality against the cached
1117
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
1118
+ // makes the direct read diverge from the cached read; a clean form
1119
+ // (same-realm OR foreign-realm) has both reads pointing at the same
1120
+ // canonical NamedNodeMap.
1121
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
1122
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
1123
+ // returns the integer 1 for any Element regardless of realm; direct
1124
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
1125
+ // the named child element. Cheap addition — nodeType is read from
1126
+ // an internal slot, no serialization cost — and removes a residual
1127
+ // clobbering surface used by several mXSS / PI / comment branches
1128
+ // in _sanitizeElements that compare currentNode.nodeType directly.
1129
+ element.nodeType !== getNodeType(element) ||
1130
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
1131
+ // "childNodes" shadows the prototype getter. Direct reads of
1132
+ // form.childNodes from a clobbered form return the named child
1133
+ // instead of the real NodeList, so any walk that reads it directly
1134
+ // skips the form's real children. Compare the direct read to the
1135
+ // cached Node.prototype getter — when the form's named-property
1136
+ // getter intercepts the read, the two values differ and we flag
1137
+ // the form. This catches every clobbering child type (input,
1138
+ // select, etc.) regardless of whether the named child happens to
1139
+ // carry a numeric .length, which a typeof-based probe would miss
1140
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
1141
+ element.childNodes !== getChildNodes(element);
1142
+ };
1143
+ /**
1144
+ * Checks whether the given value is a DocumentFragment from any realm.
1145
+ *
1146
+ * The realm-independent replacement reads `nodeType` through the cached
1147
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
1148
+ * constant (11). nodeType is a numeric value resolved from the node's
1149
+ * internal slot, identical across realms for the same kind of node.
1150
+ *
1151
+ * @param value object to check
1152
+ * @return true if value is a DocumentFragment-shaped node from any realm
1153
+ */
1154
+ const _isDocumentFragment = function _isDocumentFragment(value) {
1155
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1156
+ return false;
1157
+ }
1158
+ try {
1159
+ return getNodeType(value) === NODE_TYPE.documentFragment;
1160
+ } catch (_) {
1161
+ return false;
1162
+ }
909
1163
  };
910
1164
  /**
911
- * Checks whether the given object is a DOM node.
1165
+ * Checks whether the given object is a DOM node, including nodes that
1166
+ * originate from a different window/realm (e.g. an iframe's
1167
+ * contentDocument). The previous `value instanceof Node` check was
1168
+ * realm-bound: nodes from a different window failed it, causing
1169
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
1170
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
912
1171
  *
913
1172
  * @param value object to check whether it's a DOM node
914
- * @return true is object is a DOM node
1173
+ * @return true if value is a DOM node from any realm
915
1174
  */
916
1175
  const _isNode = function _isNode(value) {
917
- return typeof Node === 'function' && value instanceof Node;
1176
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1177
+ return false;
1178
+ }
1179
+ try {
1180
+ return typeof getNodeType(value) === 'number';
1181
+ } catch (_) {
1182
+ return false;
1183
+ }
918
1184
  };
919
1185
  function _executeHooks(hooks, currentNode, data) {
920
1186
  arrayForEach(hooks, hook => {
@@ -940,7 +1206,7 @@
940
1206
  return true;
941
1207
  }
942
1208
  /* Now let's check the element's type and name */
943
- const tagName = transformCaseFunc(currentNode.nodeName);
1209
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
944
1210
  /* Execute a hook if present */
945
1211
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
946
1212
  tagName,
@@ -977,15 +1243,21 @@
977
1243
  return false;
978
1244
  }
979
1245
  }
980
- /* Keep content except for bad-listed elements */
1246
+ /* Keep content except for bad-listed elements.
1247
+ Use the cached prototype getters exclusively — the previous code
1248
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1249
+ fallbacks, but the cached getters always return the canonical
1250
+ value (or null for a real parent-less node), so the fallback
1251
+ path was dead in safe cases and a clobbering surface in unsafe
1252
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
1253
+ parentNode)` check already gates correctly. */
981
1254
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
982
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
983
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
1255
+ const parentNode = getParentNode(currentNode);
1256
+ const childNodes = getChildNodes(currentNode);
984
1257
  if (childNodes && parentNode) {
985
1258
  const childCount = childNodes.length;
986
1259
  for (let i = childCount - 1; i >= 0; --i) {
987
1260
  const childClone = cloneNode(childNodes[i], true);
988
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
989
1261
  parentNode.insertBefore(childClone, getNextSibling(currentNode));
990
1262
  }
991
1263
  }
@@ -993,8 +1265,14 @@
993
1265
  _forceRemove(currentNode);
994
1266
  return true;
995
1267
  }
996
- /* Check whether element has a valid namespace */
997
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
1268
+ /* Check whether element has a valid namespace.
1269
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1270
+ nodeType getter rather than `instanceof Element`, which is realm-
1271
+ bound and short-circuits to false for any node minted in a different
1272
+ realm — letting a foreign-realm element with a forbidden namespace
1273
+ slip past the namespace check entirely. */
1274
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
1275
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
998
1276
  _forceRemove(currentNode);
999
1277
  return true;
1000
1278
  }
@@ -1007,7 +1285,7 @@
1007
1285
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1008
1286
  /* Get the element's text content */
1009
1287
  content = currentNode.textContent;
1010
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1288
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1011
1289
  content = stringReplace(content, expr, ' ');
1012
1290
  });
1013
1291
  if (currentNode.textContent !== content) {
@@ -1039,11 +1317,12 @@
1039
1317
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1040
1318
  return false;
1041
1319
  }
1320
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
1042
1321
  /* Allow valid data-* attributes: At least one character after "-"
1043
1322
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1044
1323
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1045
1324
  We don't need to check the value; it's always URI safe. */
1046
- 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]) {
1325
+ 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]) {
1047
1326
  if (
1048
1327
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1049
1328
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1055,11 +1334,15 @@
1055
1334
  return false;
1056
1335
  }
1057
1336
  /* Check value is safe. First, is attr inert? If so, is safe */
1058
- } 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) {
1337
+ } 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) {
1059
1338
  return false;
1060
1339
  } else ;
1061
1340
  return true;
1062
1341
  };
1342
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
1343
+ * never be treated as basic custom elements even when a permissive
1344
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
1345
+ const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']);
1063
1346
  /**
1064
1347
  * _isBasicCustomElement
1065
1348
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -1069,7 +1352,7 @@
1069
1352
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1070
1353
  */
1071
1354
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1072
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
1355
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1073
1356
  };
1074
1357
  /**
1075
1358
  * _sanitizeAttributes
@@ -1084,9 +1367,7 @@
1084
1367
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1085
1368
  /* Execute a hook if present */
1086
1369
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1087
- const {
1088
- attributes
1089
- } = currentNode;
1370
+ const attributes = currentNode.attributes;
1090
1371
  /* Check if we have attributes; if not we might have a text node */
1091
1372
  if (!attributes || _isClobbered(currentNode)) {
1092
1373
  return;
@@ -1102,11 +1383,9 @@
1102
1383
  /* Go backwards over all attributes; safely remove bad ones */
1103
1384
  while (l--) {
1104
1385
  const attr = attributes[l];
1105
- const {
1106
- name,
1107
- namespaceURI,
1108
- value: attrValue
1109
- } = attr;
1386
+ const name = attr.name,
1387
+ namespaceURI = attr.namespaceURI,
1388
+ attrValue = attr.value;
1110
1389
  const lcName = transformCaseFunc(name);
1111
1390
  const initValue = attrValue;
1112
1391
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1120,12 +1399,14 @@
1120
1399
  /* Full DOM Clobbering protection via namespace isolation,
1121
1400
  * Prefix id and name attributes with `user-content-`
1122
1401
  */
1123
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
1402
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
1124
1403
  // Remove the attribute with this value
1125
1404
  _removeAttribute(name, currentNode);
1126
1405
  // Prefix the value and later re-create the attribute with the sanitized value
1127
1406
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
1128
1407
  }
1408
+ // Else: already prefixed, leave the attribute alone — the prefix is
1409
+ // itself the clobbering protection, and re-applying it is incorrect.
1129
1410
  /* Work around a security issue with comments inside attributes */
1130
1411
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
1131
1412
  _removeAttribute(name, currentNode);
@@ -1152,7 +1433,7 @@
1152
1433
  }
1153
1434
  /* Sanitize attribute content to be template-safe */
1154
1435
  if (SAFE_FOR_TEMPLATES) {
1155
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1436
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1156
1437
  value = stringReplace(value, expr, ' ');
1157
1438
  });
1158
1439
  }
@@ -1168,7 +1449,7 @@
1168
1449
  switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1169
1450
  case 'TrustedHTML':
1170
1451
  {
1171
- value = trustedTypesPolicy.createHTML(value);
1452
+ value = _createTrustedHTML(value);
1172
1453
  break;
1173
1454
  }
1174
1455
  case 'TrustedScriptURL':
@@ -1218,14 +1499,98 @@
1218
1499
  _sanitizeElements(shadowNode);
1219
1500
  /* Check attributes next */
1220
1501
  _sanitizeAttributes(shadowNode);
1221
- /* Deep shadow DOM detected */
1222
- if (shadowNode.content instanceof DocumentFragment) {
1502
+ /* Deep shadow DOM detected.
1503
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
1504
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
1505
+ recurse into <template>.content from foreign realms too. */
1506
+ if (_isDocumentFragment(shadowNode.content)) {
1223
1507
  _sanitizeShadowDOM2(shadowNode.content);
1224
1508
  }
1509
+ /* An element iterated here may itself host an attached
1510
+ shadow root. The default NodeIterator does not enter shadow
1511
+ trees, so a shadow root nested inside template.content was
1512
+ previously reached by no walk at all (the pre-pass at
1513
+ _sanitizeAttachedShadowRoots descends via childNodes, which
1514
+ doesn't enter template.content; the template-content recursion
1515
+ above iterates the content but never inspected shadowRoot).
1516
+ Walk it explicitly. The nodeType guard avoids reading
1517
+ shadowRoot off text / comment / CDATA / PI nodes that the
1518
+ iterator also surfaces. */
1519
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
1520
+ if (shadowNodeType === NODE_TYPE.element) {
1521
+ const innerSr = getShadowRoot ? getShadowRoot(shadowNode) : shadowNode.shadowRoot;
1522
+ if (_isDocumentFragment(innerSr)) {
1523
+ _sanitizeAttachedShadowRoots2(innerSr);
1524
+ _sanitizeShadowDOM2(innerSr);
1525
+ }
1526
+ }
1225
1527
  }
1226
1528
  /* Execute a hook if present */
1227
1529
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1228
1530
  };
1531
+ /**
1532
+ * _sanitizeAttachedShadowRoots
1533
+ *
1534
+ * Walks `root` and feeds every attached shadow root we encounter into
1535
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
1536
+ * does not descend into shadow trees, so nodes inside an attached
1537
+ * shadow root would otherwise be skipped entirely.
1538
+ *
1539
+ * Two real input paths put attached shadow roots in front of us:
1540
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
1541
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
1542
+ * shadow root because it was created with `clonable: true`.
1543
+ *
1544
+ * This pass runs once, up front, so the main iteration loop (and the
1545
+ * existing _sanitizeShadowDOM template-content recursion) stay
1546
+ * untouched — string-input paths are not affected.
1547
+ *
1548
+ * @param root the subtree root to walk for attached shadow roots
1549
+ */
1550
+ const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1551
+ const nodeType = getNodeType ? getNodeType(root) : root.nodeType;
1552
+ if (nodeType === NODE_TYPE.element) {
1553
+ const sr = getShadowRoot ? getShadowRoot(root) : root.shadowRoot;
1554
+ // Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType-based
1555
+ // detection rather than `instanceof DocumentFragment`, which is
1556
+ // realm-bound and silently skipped shadow roots whose host element
1557
+ // belonged to a foreign realm (e.g. iframe.contentDocument
1558
+ // attachShadow). A foreign-realm ShadowRoot extends the foreign
1559
+ // realm's DocumentFragment, not ours, so the old instanceof check
1560
+ // returned false and the shadow subtree was never walked.
1561
+ if (_isDocumentFragment(sr)) {
1562
+ // Recurse first so that nested shadow roots are reached even if
1563
+ // _sanitizeShadowDOM removes hosts at this level.
1564
+ _sanitizeAttachedShadowRoots2(sr);
1565
+ _sanitizeShadowDOM2(sr);
1566
+ }
1567
+ }
1568
+ // Snapshot children before recursing. Sanitization of one subtree
1569
+ // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1570
+ // and naive nextSibling traversal would silently skip the rest of
1571
+ // the list once a node is detached.
1572
+ const childNodes = getChildNodes ? getChildNodes(root) : root.childNodes;
1573
+ if (!childNodes) {
1574
+ return;
1575
+ }
1576
+ const snapshot = [];
1577
+ arrayForEach(childNodes, child => {
1578
+ arrayPush(snapshot, child);
1579
+ });
1580
+ for (const child of snapshot) {
1581
+ _sanitizeAttachedShadowRoots2(child);
1582
+ }
1583
+ /* When the root is a <template>, also descend into root.content */
1584
+ if (nodeType === NODE_TYPE.element) {
1585
+ const rootName = getNodeName ? getNodeName(root) : null;
1586
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
1587
+ const content = root.content;
1588
+ if (_isDocumentFragment(content)) {
1589
+ _sanitizeAttachedShadowRoots2(content);
1590
+ }
1591
+ }
1592
+ }
1593
+ };
1229
1594
  // eslint-disable-next-line complexity
1230
1595
  DOMPurify.sanitize = function (dirty) {
1231
1596
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1242,13 +1607,9 @@
1242
1607
  }
1243
1608
  /* Stringify, in case dirty is an object */
1244
1609
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
1245
- if (typeof dirty.toString === 'function') {
1246
- dirty = dirty.toString();
1247
- if (typeof dirty !== 'string') {
1248
- throw typeErrorCreate('dirty is not a string, aborting');
1249
- }
1250
- } else {
1251
- throw typeErrorCreate('toString is not a function');
1610
+ dirty = stringifyValue(dirty);
1611
+ if (typeof dirty !== 'string') {
1612
+ throw typeErrorCreate('dirty is not a string, aborting');
1252
1613
  }
1253
1614
  }
1254
1615
  /* Return dirty HTML if DOMPurify cannot run */
@@ -1266,14 +1627,35 @@
1266
1627
  IN_PLACE = false;
1267
1628
  }
1268
1629
  if (IN_PLACE) {
1269
- /* Do some early pre-sanitization to avoid unsafe root nodes */
1270
- if (dirty.nodeName) {
1271
- const tagName = transformCaseFunc(dirty.nodeName);
1630
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
1631
+ Read nodeName through the cached prototype getter — a clobbering
1632
+ child named "nodeName" on the form root would otherwise shadow
1633
+ the property and let this check skip the root-allowlist
1634
+ validation entirely. */
1635
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
1636
+ if (typeof nn === 'string') {
1637
+ const tagName = transformCaseFunc(nn);
1272
1638
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
1273
1639
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1274
1640
  }
1275
1641
  }
1276
- } else if (dirty instanceof Node) {
1642
+ /* Pre-flight the root through _isClobbered. The iterator-driven
1643
+ removal path can not detach a parent-less root: _forceRemove
1644
+ falls through to Element.prototype.remove(), which per spec
1645
+ is a no-op on a node with no parent. A clobbered root would
1646
+ then survive the main loop with its attributes uninspected,
1647
+ because _sanitizeAttributes early-returns on _isClobbered. The
1648
+ result would be an attacker-controlled form, complete with any
1649
+ event-handler attributes the caller passed in, handed back to
1650
+ the application unsanitized. Refuse to sanitize such a root
1651
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
1652
+ if (_isClobbered(dirty)) {
1653
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
1654
+ }
1655
+ /* Sanitize attached shadow roots before the main iterator runs.
1656
+ The iterator does not descend into shadow trees. */
1657
+ _sanitizeAttachedShadowRoots2(dirty);
1658
+ } else if (_isNode(dirty)) {
1277
1659
  /* If dirty is a DOM element, append to an empty document to avoid
1278
1660
  elements being stripped by the parser */
1279
1661
  body = _initDocument('<!---->');
@@ -1287,12 +1669,18 @@
1287
1669
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1288
1670
  body.appendChild(importedNode);
1289
1671
  }
1672
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1673
+ them before the main iterator runs, since the iterator does not
1674
+ descend into shadow trees. The walk routes every read through a
1675
+ cached prototype getter so clobbering descendants on a form root
1676
+ cannot hide a shadow host from this pass. */
1677
+ _sanitizeAttachedShadowRoots2(importedNode);
1290
1678
  } else {
1291
1679
  /* Exit directly if we have nothing to do */
1292
1680
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
1293
1681
  // eslint-disable-next-line unicorn/prefer-includes
1294
1682
  dirty.indexOf('<') === -1) {
1295
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
1683
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
1296
1684
  }
1297
1685
  /* Initialize the document to work on */
1298
1686
  body = _initDocument(dirty);
@@ -1313,24 +1701,25 @@
1313
1701
  _sanitizeElements(currentNode);
1314
1702
  /* Check attributes next */
1315
1703
  _sanitizeAttributes(currentNode);
1316
- /* Shadow DOM detected, sanitize it */
1317
- if (currentNode.content instanceof DocumentFragment) {
1704
+ /* Shadow DOM detected, sanitize it.
1705
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
1706
+ instead of instanceof, so foreign-realm <template>.content is
1707
+ walked correctly. */
1708
+ if (_isDocumentFragment(currentNode.content)) {
1318
1709
  _sanitizeShadowDOM2(currentNode.content);
1319
1710
  }
1320
1711
  }
1321
1712
  /* If we sanitized `dirty` in-place, return it. */
1322
1713
  if (IN_PLACE) {
1714
+ if (SAFE_FOR_TEMPLATES) {
1715
+ _scrubTemplateExpressions2(dirty);
1716
+ }
1323
1717
  return dirty;
1324
1718
  }
1325
1719
  /* Return sanitized string or DOM */
1326
1720
  if (RETURN_DOM) {
1327
1721
  if (SAFE_FOR_TEMPLATES) {
1328
- body.normalize();
1329
- let html = body.innerHTML;
1330
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1331
- html = stringReplace(html, expr, ' ');
1332
- });
1333
- body.innerHTML = html;
1722
+ _scrubTemplateExpressions2(body);
1334
1723
  }
1335
1724
  if (RETURN_DOM_FRAGMENT) {
1336
1725
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1360,11 +1749,11 @@
1360
1749
  }
1361
1750
  /* Sanitize final string template-safe */
1362
1751
  if (SAFE_FOR_TEMPLATES) {
1363
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1752
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1364
1753
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1365
1754
  });
1366
1755
  }
1367
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
1756
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
1368
1757
  };
1369
1758
  DOMPurify.setConfig = function () {
1370
1759
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};