@vaadin/bundles 25.2.0-beta1 → 25.2.0-beta2

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