@ukic/web-components 3.28.0 → 3.30.0

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.
Files changed (114) hide show
  1. package/components/ic-accordion.js +53 -38
  2. package/components/ic-accordion.js.map +1 -1
  3. package/components/ic-breadcrumb-group.js +16 -12
  4. package/components/ic-breadcrumb-group.js.map +1 -1
  5. package/components/ic-menu-item2.js +7 -2
  6. package/components/ic-menu-item2.js.map +1 -1
  7. package/components/ic-menu2.js +1393 -307
  8. package/components/ic-menu2.js.map +1 -1
  9. package/components/ic-navigation-button.js +8 -5
  10. package/components/ic-navigation-button.js.map +1 -1
  11. package/components/ic-pagination-bar.js.map +1 -1
  12. package/components/ic-pagination-item2.js.map +1 -1
  13. package/components/ic-pagination2.js.map +1 -1
  14. package/components/ic-popover-menu.js +17 -5
  15. package/components/ic-popover-menu.js.map +1 -1
  16. package/components/ic-radio-group.js.map +1 -1
  17. package/components/ic-skip-link.js.map +1 -1
  18. package/components/ic-status-tag.js.map +1 -1
  19. package/dist/cjs/core.cjs.js +1 -1
  20. package/dist/cjs/ic-accordion.cjs.entry.js +53 -38
  21. package/dist/cjs/ic-accordion.cjs.entry.js.map +1 -1
  22. package/dist/cjs/ic-breadcrumb-group.cjs.entry.js +16 -12
  23. package/dist/cjs/ic-breadcrumb-group.cjs.entry.js.map +1 -1
  24. package/dist/cjs/ic-button_3.cjs.entry.js.map +1 -1
  25. package/dist/cjs/ic-horizontal-scroll.cjs.entry.js.map +1 -1
  26. package/dist/cjs/ic-input-component-container_3.cjs.entry.js +1393 -307
  27. package/dist/cjs/ic-input-component-container_3.cjs.entry.js.map +1 -1
  28. package/dist/cjs/ic-menu-item.cjs.entry.js +5 -1
  29. package/dist/cjs/ic-menu-item.cjs.entry.js.map +1 -1
  30. package/dist/cjs/ic-navigation-button.cjs.entry.js +8 -5
  31. package/dist/cjs/ic-navigation-button.cjs.entry.js.map +1 -1
  32. package/dist/cjs/ic-popover-menu.cjs.entry.js +16 -5
  33. package/dist/cjs/ic-popover-menu.cjs.entry.js.map +1 -1
  34. package/dist/cjs/loader.cjs.js +1 -1
  35. package/dist/collection/components/ic-accordion/ic-accordion.js +53 -38
  36. package/dist/collection/components/ic-accordion/ic-accordion.js.map +1 -1
  37. package/dist/collection/components/ic-breadcrumb-group/ic-breadcrumb-group.js +17 -13
  38. package/dist/collection/components/ic-breadcrumb-group/ic-breadcrumb-group.js.map +1 -1
  39. package/dist/collection/components/ic-menu-item/ic-menu-item.css +9 -14
  40. package/dist/collection/components/ic-menu-item/ic-menu-item.js +24 -0
  41. package/dist/collection/components/ic-menu-item/ic-menu-item.js.map +1 -1
  42. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js +8 -5
  43. package/dist/collection/components/ic-navigation-button/ic-navigation-button.js.map +1 -1
  44. package/dist/collection/components/ic-popover-menu/ic-popover-menu.js +36 -5
  45. package/dist/collection/components/ic-popover-menu/ic-popover-menu.js.map +1 -1
  46. package/dist/collection/components/ic-popover-menu/ic-popover-menu.stories.js +169 -1
  47. package/dist/components/ic-accordion.js +53 -38
  48. package/dist/components/ic-accordion.js.map +1 -1
  49. package/dist/components/ic-back-to-top.js.map +1 -1
  50. package/dist/components/ic-breadcrumb-group.js +16 -12
  51. package/dist/components/ic-breadcrumb-group.js.map +1 -1
  52. package/dist/components/ic-menu-item2.js +7 -2
  53. package/dist/components/ic-menu-item2.js.map +1 -1
  54. package/dist/components/ic-menu2.js +1393 -307
  55. package/dist/components/ic-menu2.js.map +1 -1
  56. package/dist/components/ic-navigation-button.js +8 -5
  57. package/dist/components/ic-navigation-button.js.map +1 -1
  58. package/dist/components/ic-popover-menu.js +17 -5
  59. package/dist/components/ic-popover-menu.js.map +1 -1
  60. package/dist/components/ic-radio-group.js.map +1 -1
  61. package/dist/components/ic-radio-option.js.map +1 -1
  62. package/dist/components/ic-side-navigation.js.map +1 -1
  63. package/dist/components/ic-skip-link.js.map +1 -1
  64. package/dist/components/ic-status-tag.js.map +1 -1
  65. package/dist/core/core.esm.js +1 -1
  66. package/dist/core/core.esm.js.map +1 -1
  67. package/dist/core/{p-9ced00a7.entry.js → p-41bbdc4c.entry.js} +2 -2
  68. package/dist/core/{p-9ced00a7.entry.js.map → p-41bbdc4c.entry.js.map} +1 -1
  69. package/dist/core/p-8fc5e9f9.entry.js.map +1 -1
  70. package/dist/core/p-98dee727.entry.js.map +1 -1
  71. package/dist/core/{p-fc4b7d2b.entry.js → p-9cf35c23.entry.js} +2 -2
  72. package/dist/core/p-9cf35c23.entry.js.map +1 -0
  73. package/dist/core/p-be8bbed8.entry.js +2 -0
  74. package/dist/core/p-be8bbed8.entry.js.map +1 -0
  75. package/dist/core/p-ccba84b7.entry.js +3 -0
  76. package/dist/core/p-ccba84b7.entry.js.map +1 -0
  77. package/dist/core/p-ef289935.entry.js +2 -0
  78. package/dist/core/p-ef289935.entry.js.map +1 -0
  79. package/dist/core/p-f8aa89ff.entry.js +2 -0
  80. package/dist/core/p-f8aa89ff.entry.js.map +1 -0
  81. package/dist/esm/core.js +1 -1
  82. package/dist/esm/ic-accordion.entry.js +53 -38
  83. package/dist/esm/ic-accordion.entry.js.map +1 -1
  84. package/dist/esm/ic-breadcrumb-group.entry.js +16 -12
  85. package/dist/esm/ic-breadcrumb-group.entry.js.map +1 -1
  86. package/dist/esm/ic-button_3.entry.js.map +1 -1
  87. package/dist/esm/ic-horizontal-scroll.entry.js.map +1 -1
  88. package/dist/esm/ic-input-component-container_3.entry.js +1393 -307
  89. package/dist/esm/ic-input-component-container_3.entry.js.map +1 -1
  90. package/dist/esm/ic-menu-item.entry.js +5 -1
  91. package/dist/esm/ic-menu-item.entry.js.map +1 -1
  92. package/dist/esm/ic-navigation-button.entry.js +8 -5
  93. package/dist/esm/ic-navigation-button.entry.js.map +1 -1
  94. package/dist/esm/ic-popover-menu.entry.js +16 -5
  95. package/dist/esm/ic-popover-menu.entry.js.map +1 -1
  96. package/dist/esm/loader.js +1 -1
  97. package/dist/types/components/ic-accordion/ic-accordion.d.ts +2 -0
  98. package/dist/types/components/ic-breadcrumb-group/ic-breadcrumb-group.d.ts +6 -1
  99. package/dist/types/components/ic-menu-item/ic-menu-item.d.ts +4 -0
  100. package/dist/types/components/ic-popover-menu/ic-popover-menu.d.ts +4 -0
  101. package/dist/types/components.d.ts +18 -2
  102. package/hydrate/index.js +1494 -369
  103. package/hydrate/index.mjs +1494 -369
  104. package/package.json +10 -6
  105. package/vscode-data.json +20 -1
  106. package/dist/core/p-2b1ce5b7.entry.js +0 -2
  107. package/dist/core/p-2b1ce5b7.entry.js.map +0 -1
  108. package/dist/core/p-56ab6fb7.entry.js +0 -2
  109. package/dist/core/p-56ab6fb7.entry.js.map +0 -1
  110. package/dist/core/p-d5fcd4f9.entry.js +0 -2
  111. package/dist/core/p-d5fcd4f9.entry.js.map +0 -1
  112. package/dist/core/p-fc4b7d2b.entry.js.map +0 -1
  113. package/dist/core/p-fdc4376e.entry.js +0 -3
  114. package/dist/core/p-fdc4376e.entry.js.map +0 -1
@@ -11,28 +11,69 @@ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof win
11
11
 
12
12
  var purify = {exports: {}};
13
13
 
14
- /*! @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 */
14
+ /*! @license DOMPurify 3.4.13 | (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.13/LICENSE */
15
15
 
16
16
  (function (module, exports) {
17
17
  (function (global, factory) {
18
18
  module.exports = factory() ;
19
19
  })(commonjsGlobal, (function () {
20
- const {
21
- entries,
22
- setPrototypeOf,
23
- isFrozen,
24
- getPrototypeOf,
25
- getOwnPropertyDescriptor
26
- } = Object;
27
- let {
28
- freeze,
29
- seal,
30
- create
31
- } = Object; // eslint-disable-line import/no-mutable-exports
32
- let {
33
- apply,
34
- construct
35
- } = typeof Reflect !== 'undefined' && Reflect;
20
+ function _arrayLikeToArray(r, a) {
21
+ (null == a || a > r.length) && (a = r.length);
22
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
23
+ return n;
24
+ }
25
+ function _arrayWithHoles(r) {
26
+ if (Array.isArray(r)) return r;
27
+ }
28
+ function _iterableToArrayLimit(r, l) {
29
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
30
+ if (null != t) {
31
+ var e,
32
+ n,
33
+ i,
34
+ u,
35
+ a = [],
36
+ f = true,
37
+ o = false;
38
+ try {
39
+ 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);
40
+ } catch (r) {
41
+ o = true, n = r;
42
+ } finally {
43
+ try {
44
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
45
+ } finally {
46
+ if (o) throw n;
47
+ }
48
+ }
49
+ return a;
50
+ }
51
+ }
52
+ function _nonIterableRest() {
53
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
54
+ }
55
+ function _slicedToArray(r, e) {
56
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
57
+ }
58
+ function _unsupportedIterableToArray(r, a) {
59
+ if (r) {
60
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
61
+ var t = {}.toString.call(r).slice(8, -1);
62
+ 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;
63
+ }
64
+ }
65
+
66
+ const entries = Object.entries,
67
+ setPrototypeOf = Object.setPrototypeOf,
68
+ isFrozen = Object.isFrozen,
69
+ getPrototypeOf = Object.getPrototypeOf,
70
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
71
+ let freeze = Object.freeze,
72
+ seal = Object.seal,
73
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
74
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
75
+ apply = _ref.apply,
76
+ construct = _ref.construct;
36
77
  if (!freeze) {
37
78
  freeze = function freeze(x) {
38
79
  return x;
@@ -64,13 +105,19 @@ var purify = {exports: {}};
64
105
  const arrayPop = unapply(Array.prototype.pop);
65
106
  const arrayPush = unapply(Array.prototype.push);
66
107
  const arraySplice = unapply(Array.prototype.splice);
108
+ const arrayIsArray = Array.isArray;
67
109
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
68
110
  const stringToString = unapply(String.prototype.toString);
69
111
  const stringMatch = unapply(String.prototype.match);
70
112
  const stringReplace = unapply(String.prototype.replace);
71
113
  const stringIndexOf = unapply(String.prototype.indexOf);
72
114
  const stringTrim = unapply(String.prototype.trim);
115
+ const numberToString = unapply(Number.prototype.toString);
116
+ const booleanToString = unapply(Boolean.prototype.toString);
117
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
118
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
73
119
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
120
+ const objectToString = unapply(Object.prototype.toString);
74
121
  const regExpTest = unapply(RegExp.prototype.test);
75
122
  const typeErrorCreate = unconstruct(TypeError);
76
123
  /**
@@ -120,6 +167,9 @@ var purify = {exports: {}};
120
167
  // Prevent prototype setters from intercepting set as a this value.
121
168
  setPrototypeOf(set, null);
122
169
  }
170
+ if (!arrayIsArray(array)) {
171
+ return set;
172
+ }
123
173
  let l = array.length;
124
174
  while (l--) {
125
175
  let element = array[l];
@@ -160,10 +210,13 @@ var purify = {exports: {}};
160
210
  */
161
211
  function clone(object) {
162
212
  const newObject = create(null);
163
- for (const [property, value] of entries(object)) {
213
+ for (const _ref2 of entries(object)) {
214
+ var _ref3 = _slicedToArray(_ref2, 2);
215
+ const property = _ref3[0];
216
+ const value = _ref3[1];
164
217
  const isPropertyExist = objectHasOwnProperty(object, property);
165
218
  if (isPropertyExist) {
166
- if (Array.isArray(value)) {
219
+ if (arrayIsArray(value)) {
167
220
  newObject[property] = cleanArray(value);
168
221
  } else if (value && typeof value === 'object' && value.constructor === Object) {
169
222
  newObject[property] = clone(value);
@@ -174,6 +227,58 @@ var purify = {exports: {}};
174
227
  }
175
228
  return newObject;
176
229
  }
230
+ /**
231
+ * Convert non-node values into strings without depending on direct property access.
232
+ *
233
+ * @param value - The value to stringify.
234
+ * @returns A string representation of the provided value.
235
+ */
236
+ function stringifyValue(value) {
237
+ switch (typeof value) {
238
+ case 'string':
239
+ {
240
+ return value;
241
+ }
242
+ case 'number':
243
+ {
244
+ return numberToString(value);
245
+ }
246
+ case 'boolean':
247
+ {
248
+ return booleanToString(value);
249
+ }
250
+ case 'bigint':
251
+ {
252
+ return bigintToString ? bigintToString(value) : '0';
253
+ }
254
+ case 'symbol':
255
+ {
256
+ return symbolToString ? symbolToString(value) : 'Symbol()';
257
+ }
258
+ case 'undefined':
259
+ {
260
+ return objectToString(value);
261
+ }
262
+ case 'function':
263
+ case 'object':
264
+ {
265
+ if (value === null) {
266
+ return objectToString(value);
267
+ }
268
+ const valueAsRecord = value;
269
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
270
+ if (typeof valueToString === 'function') {
271
+ const stringified = valueToString(valueAsRecord);
272
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
273
+ }
274
+ return objectToString(value);
275
+ }
276
+ default:
277
+ {
278
+ return objectToString(value);
279
+ }
280
+ }
281
+ }
177
282
  /**
178
283
  * This method automatically checks if the prop is function or getter and behaves accordingly.
179
284
  *
@@ -199,6 +304,14 @@ var purify = {exports: {}};
199
304
  }
200
305
  return fallbackValue;
201
306
  }
307
+ function isRegex(value) {
308
+ try {
309
+ regExpTest(value, '');
310
+ return true;
311
+ } catch (_unused) {
312
+ return false;
313
+ }
314
+ }
202
315
 
203
316
  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']);
204
317
  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']);
@@ -214,15 +327,14 @@ var purify = {exports: {}};
214
327
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
215
328
  const text = freeze(['#text']);
216
329
 
217
- 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']);
218
- 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']);
330
+ 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']);
331
+ 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', 'dominant-baseline', '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-orientation', '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']);
219
332
  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']);
220
333
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
221
334
 
222
- // eslint-disable-next-line unicorn/better-regex
223
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
224
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
225
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
335
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
336
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
337
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
226
338
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
227
339
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
228
340
  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
@@ -232,30 +344,31 @@ var purify = {exports: {}};
232
344
  );
233
345
  const DOCTYPE_NAME = seal(/^html$/i);
234
346
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
347
+ // Markup-significant character probes used by _sanitizeElements.
348
+ // Shared module-level instances are safe despite the sticky /g flags:
349
+ // unapply() resets lastIndex for RegExp receivers before every call.
350
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
351
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
352
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
353
+ const SELF_CLOSING_TAG = seal(/\/>/i);
235
354
 
236
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
237
- __proto__: null,
238
- ARIA_ATTR: ARIA_ATTR,
239
- ATTR_WHITESPACE: ATTR_WHITESPACE,
240
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
241
- DATA_ATTR: DATA_ATTR,
242
- DOCTYPE_NAME: DOCTYPE_NAME,
243
- ERB_EXPR: ERB_EXPR,
244
- IS_ALLOWED_URI: IS_ALLOWED_URI,
245
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
246
- MUSTACHE_EXPR: MUSTACHE_EXPR,
247
- TMPLIT_EXPR: TMPLIT_EXPR
248
- });
249
-
250
- /* eslint-disable @typescript-eslint/indent */
251
355
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
252
356
  const NODE_TYPE = {
253
357
  element: 1,
358
+ attribute: 2,
254
359
  text: 3,
360
+ cdataSection: 4,
361
+ entityReference: 5,
255
362
  // Deprecated
256
- progressingInstruction: 7,
363
+ entityNode: 6,
364
+ // Deprecated
365
+ processingInstruction: 7,
257
366
  comment: 8,
258
- document: 9};
367
+ document: 9,
368
+ documentType: 10,
369
+ documentFragment: 11,
370
+ notation: 12 // Deprecated
371
+ };
259
372
  const getGlobal = function getGlobal() {
260
373
  return typeof window === 'undefined' ? null : window;
261
374
  };
@@ -310,10 +423,25 @@ var purify = {exports: {}};
310
423
  uponSanitizeShadowNode: []
311
424
  };
312
425
  };
426
+ /**
427
+ * Resolve a set-valued configuration option: a fresh set built from
428
+ * cfg[key] when it is an own array property (seeded with a clone of
429
+ * options.base when given, case-normalized via options.transform),
430
+ * the fallback set otherwise.
431
+ *
432
+ * @param cfg the cloned, prototype-free configuration object
433
+ * @param key the configuration property to read
434
+ * @param fallback the set to use when the option is absent or not an array
435
+ * @param options transform and optional base set to merge into
436
+ * @returns the resolved set
437
+ */
438
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
439
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
440
+ };
313
441
  function createDOMPurify() {
314
442
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
315
443
  const DOMPurify = root => createDOMPurify(root);
316
- DOMPurify.version = '3.4.0';
444
+ DOMPurify.version = '3.4.13';
317
445
  DOMPurify.removed = [];
318
446
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
319
447
  // Not running in a browser, provide a factory function
@@ -321,28 +449,26 @@ var purify = {exports: {}};
321
449
  DOMPurify.isSupported = false;
322
450
  return DOMPurify;
323
451
  }
324
- let {
325
- document
326
- } = window;
452
+ let document = window.document;
327
453
  const originalDocument = document;
328
454
  const currentScript = originalDocument.currentScript;
329
- const {
330
- DocumentFragment,
331
- HTMLTemplateElement,
332
- Node,
333
- Element,
334
- NodeFilter,
335
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
336
- HTMLFormElement,
337
- DOMParser,
338
- trustedTypes
339
- } = window;
455
+ const HTMLTemplateElement = window.HTMLTemplateElement,
456
+ Node = window.Node,
457
+ Element = window.Element,
458
+ NodeFilter = window.NodeFilter;
459
+ const DOMParser = window.DOMParser,
460
+ trustedTypes = window.trustedTypes;
340
461
  const ElementPrototype = Element.prototype;
341
462
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
342
463
  const remove = lookupGetter(ElementPrototype, 'remove');
343
464
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
344
465
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
345
466
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
467
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
468
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
469
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
470
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
471
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
346
472
  // As per issue #47, the web-components registry is inherited by a
347
473
  // new document created via createHTMLDocument. As per the spec
348
474
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -357,33 +483,74 @@ var purify = {exports: {}};
357
483
  }
358
484
  let trustedTypesPolicy;
359
485
  let emptyHTML = '';
360
- const {
361
- implementation,
362
- createNodeIterator,
363
- createDocumentFragment,
364
- getElementsByTagName
365
- } = document;
366
- const {
367
- importNode
368
- } = originalDocument;
486
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
487
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
488
+ // on duplicate policy names — and is the only policy allowed to persist
489
+ // across configurations and survive `clearConfig()`.
490
+ let defaultTrustedTypesPolicy;
491
+ let defaultTrustedTypesPolicyResolved = false;
492
+ // Tracks whether we are already inside a call to the configured Trusted Types
493
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
494
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
495
+ // re-enter the policy and recurse until the stack overflows. We detect that
496
+ // re-entry and throw a clear, actionable error instead. The guard is shared
497
+ // across both callbacks, because either one re-entering `sanitize` triggers
498
+ // the same unbounded recursion.
499
+ let IN_TRUSTED_TYPES_POLICY = 0;
500
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
501
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
502
+ throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or ' + 'createScriptURL) must not call DOMPurify.sanitize, as that causes ' + 'infinite recursion. Do not pass a policy whose callbacks wrap ' + 'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' + 'Types" section of the README.');
503
+ }
504
+ };
505
+ const _createTrustedHTML = function _createTrustedHTML(html) {
506
+ _assertNotInTrustedTypesPolicy();
507
+ IN_TRUSTED_TYPES_POLICY++;
508
+ try {
509
+ return trustedTypesPolicy.createHTML(html);
510
+ } finally {
511
+ IN_TRUSTED_TYPES_POLICY--;
512
+ }
513
+ };
514
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
515
+ _assertNotInTrustedTypesPolicy();
516
+ IN_TRUSTED_TYPES_POLICY++;
517
+ try {
518
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
519
+ } finally {
520
+ IN_TRUSTED_TYPES_POLICY--;
521
+ }
522
+ };
523
+ // Lazily resolve (and cache) the instance's internal default policy.
524
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
525
+ // repeated (Trusted Types throws on duplicate names), and a failed or
526
+ // unsupported attempt must not be retried on every parse.
527
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
528
+ if (!defaultTrustedTypesPolicyResolved) {
529
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
530
+ defaultTrustedTypesPolicyResolved = true;
531
+ }
532
+ return defaultTrustedTypesPolicy;
533
+ };
534
+ const _document = document,
535
+ implementation = _document.implementation,
536
+ createNodeIterator = _document.createNodeIterator,
537
+ createDocumentFragment = _document.createDocumentFragment,
538
+ getElementsByTagName = _document.getElementsByTagName;
539
+ const importNode = originalDocument.importNode;
369
540
  let hooks = _createHooksMap();
370
541
  /**
371
542
  * Expose whether this browser supports running the full DOMPurify.
372
543
  */
373
544
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
374
- const {
375
- MUSTACHE_EXPR,
376
- ERB_EXPR,
377
- TMPLIT_EXPR,
378
- DATA_ATTR,
379
- ARIA_ATTR,
380
- IS_SCRIPT_OR_DATA,
381
- ATTR_WHITESPACE,
382
- CUSTOM_ELEMENT
383
- } = EXPRESSIONS;
384
- let {
385
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
386
- } = EXPRESSIONS;
545
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
546
+ ERB_EXPR$1 = ERB_EXPR,
547
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
548
+ DATA_ATTR$1 = DATA_ATTR,
549
+ ARIA_ATTR$1 = ARIA_ATTR,
550
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
551
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
552
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
553
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
387
554
  /**
388
555
  * We consider the elements and attributes below to be safe. Ideally
389
556
  * don't add any new ones but feel free to remove unwanted ones.
@@ -460,6 +627,13 @@ var purify = {exports: {}};
460
627
  let WHOLE_DOCUMENT = false;
461
628
  /* Track whether config is already set on this instance of DOMPurify. */
462
629
  let SET_CONFIG = false;
630
+ /* Pristine allowlist bindings captured at setConfig() time. On the
631
+ * persistent-config path sanitize() restores the sets from these before
632
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
633
+ * carry across calls. Null until setConfig() is called; reset by
634
+ * clearConfig(). */
635
+ let SET_CONFIG_ALLOWED_TAGS = null;
636
+ let SET_CONFIG_ALLOWED_ATTR = null;
463
637
  /* Decide if all elements (e.g. style, script) must be children of
464
638
  * document.body. By default, browsers might move them to document.head */
465
639
  let FORCE_BODY = false;
@@ -502,7 +676,17 @@ var purify = {exports: {}};
502
676
  let USE_PROFILES = {};
503
677
  /* Tags to ignore content of when KEEP_CONTENT is true */
504
678
  let FORBID_CONTENTS = null;
505
- const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
679
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
680
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
681
+ // the UA (customizable <select>) — including any on* handlers — and the
682
+ // engine re-mirrors synchronously whenever a removal changes which
683
+ // option/selectedcontent is current, even inside DOMPurify's inert
684
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
685
+ // mirror target ahead of the walk, which the engine refills, looping
686
+ // forever (DoS) and amplifying output. Dropping its content on removal
687
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
688
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
689
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
506
690
  /* Tags that are safe for data: URIs */
507
691
  let DATA_URI_TAGS = null;
508
692
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -518,8 +702,10 @@ var purify = {exports: {}};
518
702
  /* Allowed XHTML+XML namespaces */
519
703
  let ALLOWED_NAMESPACES = null;
520
704
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
521
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
522
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
705
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
706
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
707
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
708
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
523
709
  // Certain elements are allowed in both SVG and HTML
524
710
  // namespace. We need to specify them explicitly
525
711
  // so that they don't get erroneously deleted from
@@ -561,15 +747,33 @@ var purify = {exports: {}};
561
747
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
562
748
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
563
749
  /* Set configuration parameters */
564
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
565
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
566
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
567
- 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;
568
- 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;
569
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
570
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
571
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
572
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
750
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
751
+ transform: transformCaseFunc
752
+ });
753
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
754
+ transform: transformCaseFunc
755
+ });
756
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
757
+ transform: stringToString
758
+ });
759
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
760
+ transform: transformCaseFunc,
761
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
762
+ });
763
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
764
+ transform: transformCaseFunc,
765
+ base: DEFAULT_DATA_URI_TAGS
766
+ });
767
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
768
+ transform: transformCaseFunc
769
+ });
770
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
771
+ transform: transformCaseFunc
772
+ });
773
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
774
+ transform: transformCaseFunc
775
+ });
776
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
573
777
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
574
778
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
575
779
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -585,20 +789,22 @@ var purify = {exports: {}};
585
789
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
586
790
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
587
791
  IN_PLACE = cfg.IN_PLACE || false; // Default false
588
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
589
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
590
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
591
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
592
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || create(null);
593
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
594
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
595
- }
596
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
597
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
598
- }
599
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
600
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
601
- }
792
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
793
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
794
+ 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({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
795
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
796
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
797
+ CUSTOM_ELEMENT_HANDLING = create(null);
798
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
799
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
800
+ }
801
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
802
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
803
+ }
804
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
805
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
806
+ }
807
+ seal(CUSTOM_ELEMENT_HANDLING);
602
808
  if (SAFE_FOR_TEMPLATES) {
603
809
  ALLOW_DATA_ATTR = false;
604
810
  }
@@ -634,36 +840,36 @@ var purify = {exports: {}};
634
840
  EXTRA_ELEMENT_HANDLING.tagCheck = null;
635
841
  EXTRA_ELEMENT_HANDLING.attributeCheck = null;
636
842
  /* Merge configuration parameters */
637
- if (cfg.ADD_TAGS) {
843
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
638
844
  if (typeof cfg.ADD_TAGS === 'function') {
639
845
  EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
640
- } else {
846
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
641
847
  if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
642
848
  ALLOWED_TAGS = clone(ALLOWED_TAGS);
643
849
  }
644
850
  addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
645
851
  }
646
852
  }
647
- if (cfg.ADD_ATTR) {
853
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
648
854
  if (typeof cfg.ADD_ATTR === 'function') {
649
855
  EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
650
- } else {
856
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
651
857
  if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
652
858
  ALLOWED_ATTR = clone(ALLOWED_ATTR);
653
859
  }
654
860
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
655
861
  }
656
862
  }
657
- if (cfg.ADD_URI_SAFE_ATTR) {
863
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
658
864
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
659
865
  }
660
- if (cfg.FORBID_CONTENTS) {
866
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
661
867
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
662
868
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
663
869
  }
664
870
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
665
871
  }
666
- if (cfg.ADD_FORBID_CONTENTS) {
872
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
667
873
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
668
874
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
669
875
  }
@@ -682,6 +888,13 @@ var purify = {exports: {}};
682
888
  addToSet(ALLOWED_TAGS, ['tbody']);
683
889
  delete FORBID_TAGS.tbody;
684
890
  }
891
+ // Re-derive the active Trusted Types policy from this configuration on
892
+ // every parse. The active policy must never be sticky closure state that
893
+ // outlives the config that set it: a caller-supplied policy left in place
894
+ // after `clearConfig()` — or after a later call that supplied none, or
895
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
896
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
897
+ // See GHSA-vxr8-fq34-vvx9.
685
898
  if (cfg.TRUSTED_TYPES_POLICY) {
686
899
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
687
900
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -689,18 +902,45 @@ var purify = {exports: {}};
689
902
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
690
903
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
691
904
  }
692
- // Overwrite existing TrustedTypes policy.
905
+ // A caller-supplied policy applies to this configuration only.
906
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
693
907
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
694
- // Sign local variables required by `sanitize`.
695
- emptyHTML = trustedTypesPolicy.createHTML('');
908
+ // Sign local variables required by `sanitize`. If the supplied policy's
909
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
910
+ // throws via the re-entrancy guard. Restore the previous policy first so
911
+ // the instance is not left in a poisoned state. See #1422.
912
+ try {
913
+ emptyHTML = _createTrustedHTML('');
914
+ } catch (error) {
915
+ trustedTypesPolicy = previousTrustedTypesPolicy;
916
+ throw error;
917
+ }
918
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
919
+ // Explicit opt-out for this call: perform no Trusted Types signing and
920
+ // create nothing (so a strict `trusted-types` CSP that disallows a
921
+ // `dompurify` policy can still call `sanitize` from inside its own
922
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
923
+ // `null` also drops any previously retained caller policy, so it cannot
924
+ // resurface on a later call, while still allowing the next config-less
925
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
926
+ trustedTypesPolicy = undefined;
927
+ emptyHTML = '';
696
928
  } else {
697
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
929
+ // No policy supplied: keep the currently active policy if one is set — a
930
+ // previously supplied policy is intentionally sticky across config-less
931
+ // calls — otherwise fall back to the instance's own internal policy,
932
+ // created at most once. (A policy supplied for a *single* call still
933
+ // lingers by design; what must not linger is a policy whose configuration
934
+ // has been torn down via `clearConfig()`, which restores the default.)
698
935
  if (trustedTypesPolicy === undefined) {
699
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
936
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
700
937
  }
701
- // If creating the internal policy succeeded sign internal variables.
702
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
703
- emptyHTML = trustedTypesPolicy.createHTML('');
938
+ // Sign internal variables only when a policy is active. A falsy policy
939
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
940
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
941
+ // a non-policy and throw. See #1422.
942
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
943
+ emptyHTML = _createTrustedHTML('');
704
944
  }
705
945
  }
706
946
  // Prevent further manipulation of configuration.
@@ -715,6 +955,77 @@ var purify = {exports: {}};
715
955
  * correctly. */
716
956
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
717
957
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
958
+ /**
959
+ * Namespace rules for an element in the SVG namespace.
960
+ *
961
+ * @param tagName the element's lowercase tag name
962
+ * @param parent the (possibly simulated) parent node
963
+ * @param parentTagName the parent's lowercase tag name
964
+ * @returns true if a spec-compliant parser could produce this element
965
+ */
966
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
967
+ // The only way to switch from HTML namespace to SVG
968
+ // is via <svg>. If it happens via any other tag, then
969
+ // it should be killed.
970
+ if (parent.namespaceURI === HTML_NAMESPACE) {
971
+ return tagName === 'svg';
972
+ }
973
+ // The only way to switch from MathML to SVG is via <svg>
974
+ // if the parent is either <annotation-xml> or a MathML
975
+ // text integration point.
976
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
977
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
978
+ }
979
+ // We only allow elements that are defined in SVG
980
+ // spec. All others are disallowed in SVG namespace.
981
+ return Boolean(ALL_SVG_TAGS[tagName]);
982
+ };
983
+ /**
984
+ * Namespace rules for an element in the MathML namespace.
985
+ *
986
+ * @param tagName the element's lowercase tag name
987
+ * @param parent the (possibly simulated) parent node
988
+ * @param parentTagName the parent's lowercase tag name
989
+ * @returns true if a spec-compliant parser could produce this element
990
+ */
991
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
992
+ // The only way to switch from HTML namespace to MathML
993
+ // is via <math>. If it happens via any other tag, then
994
+ // it should be killed.
995
+ if (parent.namespaceURI === HTML_NAMESPACE) {
996
+ return tagName === 'math';
997
+ }
998
+ // The only way to switch from SVG to MathML is via
999
+ // <math> and HTML integration points
1000
+ if (parent.namespaceURI === SVG_NAMESPACE) {
1001
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1002
+ }
1003
+ // We only allow elements that are defined in MathML
1004
+ // spec. All others are disallowed in MathML namespace.
1005
+ return Boolean(ALL_MATHML_TAGS[tagName]);
1006
+ };
1007
+ /**
1008
+ * Namespace rules for an element in the HTML namespace.
1009
+ *
1010
+ * @param tagName the element's lowercase tag name
1011
+ * @param parent the (possibly simulated) parent node
1012
+ * @param parentTagName the parent's lowercase tag name
1013
+ * @returns true if a spec-compliant parser could produce this element
1014
+ */
1015
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
1016
+ // The only way to switch from SVG to HTML is via
1017
+ // HTML integration points, and from MathML to HTML
1018
+ // is via MathML text integration points
1019
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
1020
+ return false;
1021
+ }
1022
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
1023
+ return false;
1024
+ }
1025
+ // We disallow tags that are specific for MathML
1026
+ // or SVG and should never appear in HTML namespace
1027
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1028
+ };
718
1029
  /**
719
1030
  * @param element a DOM element whose namespace is being checked
720
1031
  * @returns Return false if the element has a
@@ -737,51 +1048,13 @@ var purify = {exports: {}};
737
1048
  return false;
738
1049
  }
739
1050
  if (element.namespaceURI === SVG_NAMESPACE) {
740
- // The only way to switch from HTML namespace to SVG
741
- // is via <svg>. If it happens via any other tag, then
742
- // it should be killed.
743
- if (parent.namespaceURI === HTML_NAMESPACE) {
744
- return tagName === 'svg';
745
- }
746
- // The only way to switch from MathML to SVG is via`
747
- // svg if parent is either <annotation-xml> or MathML
748
- // text integration points.
749
- if (parent.namespaceURI === MATHML_NAMESPACE) {
750
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
751
- }
752
- // We only allow elements that are defined in SVG
753
- // spec. All others are disallowed in SVG namespace.
754
- return Boolean(ALL_SVG_TAGS[tagName]);
1051
+ return _checkSvgNamespace(tagName, parent, parentTagName);
755
1052
  }
756
1053
  if (element.namespaceURI === MATHML_NAMESPACE) {
757
- // The only way to switch from HTML namespace to MathML
758
- // is via <math>. If it happens via any other tag, then
759
- // it should be killed.
760
- if (parent.namespaceURI === HTML_NAMESPACE) {
761
- return tagName === 'math';
762
- }
763
- // The only way to switch from SVG to MathML is via
764
- // <math> and HTML integration points
765
- if (parent.namespaceURI === SVG_NAMESPACE) {
766
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
767
- }
768
- // We only allow elements that are defined in MathML
769
- // spec. All others are disallowed in MathML namespace.
770
- return Boolean(ALL_MATHML_TAGS[tagName]);
1054
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
771
1055
  }
772
1056
  if (element.namespaceURI === HTML_NAMESPACE) {
773
- // The only way to switch from SVG to HTML is via
774
- // HTML integration points, and from MathML to HTML
775
- // is via MathML text integration points
776
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
777
- return false;
778
- }
779
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
780
- return false;
781
- }
782
- // We disallow tags that are specific for MathML
783
- // or SVG and should never appear in HTML namespace
784
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1057
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
785
1058
  }
786
1059
  // For XHTML and XML documents that support custom namespaces
787
1060
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -806,7 +1079,81 @@ var purify = {exports: {}};
806
1079
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
807
1080
  getParentNode(node).removeChild(node);
808
1081
  } catch (_) {
1082
+ /* The normal detach failed — this is reached for a parentless node
1083
+ (getParentNode() is null, so .removeChild throws). Element.prototype
1084
+ .remove() is itself a spec no-op on a parentless node, so a recorded
1085
+ "removal" would otherwise hand the caller back an intact,
1086
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
1087
+ the style-with-element-child rule decided to kill). Fail closed by
1088
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
1089
+ rather than trying to "neutralize" the node via its own methods.
1090
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
1091
+ on the node, both of which a <form> root can clobber via a named child
1092
+ (and _isClobbered does not even probe getAttributeNames), so the
1093
+ neutralize step could itself be silently defeated, leaving the payload
1094
+ intact. A throw touches only the cached, clobber-safe remove() and
1095
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
1096
+ to every root-kill reason. REPORT-3.
1097
+ This lives inside the catch, so it never fires for a normally-removed
1098
+ in-tree node: those have a parent, removeChild() succeeds, and the
1099
+ catch is not entered. Only a kept (parentless) root reaches here. */
809
1100
  remove(node);
1101
+ if (!getParentNode(node)) {
1102
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
1103
+ }
1104
+ }
1105
+ };
1106
+ /**
1107
+ * _neutralizeRoot
1108
+ *
1109
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
1110
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1111
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1112
+ * parentless guard throws, or any other re-entrant engine mutation — would
1113
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
1114
+ * after the abort point still carrying its handlers. There is no safe way
1115
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
1116
+ * remove every child and every attribute, then let the caller's catch see
1117
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1118
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1119
+ *
1120
+ * @param root the in-place root to empty
1121
+ */
1122
+ const _neutralizeRoot = function _neutralizeRoot(root) {
1123
+ /* Strip every disallowed attribute (on* handlers included) off the whole
1124
+ subtree BEFORE detaching anything. Detaching first would hand back
1125
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
1126
+ whose queued resource event still fires in page scope after we throw.
1127
+ Clobber-safe reads; a doomed clobbered node's own attributes are
1128
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
1129
+ _neutralizeSubtree(root);
1130
+ const childNodes = getChildNodes(root);
1131
+ if (childNodes) {
1132
+ const snapshot = [];
1133
+ arrayForEach(childNodes, child => {
1134
+ arrayPush(snapshot, child);
1135
+ });
1136
+ arrayForEach(snapshot, child => {
1137
+ try {
1138
+ remove(child);
1139
+ } catch (_) {
1140
+ /* Best-effort teardown; a still-attached child is handled below */
1141
+ }
1142
+ });
1143
+ }
1144
+ const attributes = getAttributes(root);
1145
+ if (attributes) {
1146
+ for (let i = attributes.length - 1; i >= 0; --i) {
1147
+ const attribute = attributes[i];
1148
+ const name = attribute && attribute.name;
1149
+ if (typeof name === 'string') {
1150
+ try {
1151
+ root.removeAttribute(name);
1152
+ } catch (_) {
1153
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
1154
+ }
1155
+ }
1156
+ }
810
1157
  }
811
1158
  };
812
1159
  /**
@@ -841,6 +1188,148 @@ var purify = {exports: {}};
841
1188
  }
842
1189
  }
843
1190
  };
1191
+ /**
1192
+ * _stripDisallowedAttributes
1193
+ *
1194
+ * Removes every attribute the active configuration does not allow from a
1195
+ * single element, using the same allowlist as the main attribute pass (so
1196
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1197
+ * neutralise nodes that are being discarded from an in-place tree.
1198
+ *
1199
+ * @param element the element to strip
1200
+ */
1201
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
1202
+ const attributes = getAttributes(element);
1203
+ if (!attributes) {
1204
+ return;
1205
+ }
1206
+ for (let i = attributes.length - 1; i >= 0; --i) {
1207
+ const attribute = attributes[i];
1208
+ const name = attribute && attribute.name;
1209
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
1210
+ continue;
1211
+ }
1212
+ try {
1213
+ element.removeAttribute(name);
1214
+ } catch (_) {
1215
+ /* Clobbered removeAttribute on a doomed node — ignore */
1216
+ }
1217
+ }
1218
+ };
1219
+ /**
1220
+ * _neutralizeSubtree
1221
+ *
1222
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1223
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1224
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1225
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1226
+ * those dropped nodes are detached from the caller's LIVE tree but a
1227
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1228
+ * loading) keeps its queued resource event, which fires in page scope after
1229
+ * sanitize returns. This walks a removed subtree and strips every attribute
1230
+ * the active configuration does not allow — so `on*` handlers are cancelled
1231
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1232
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
1233
+ * queued event can fire. Hook-free by design: these nodes leave the output,
1234
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1235
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1236
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1237
+ * the `<img>`, are reached and scrubbed).
1238
+ *
1239
+ * @param root the root of a removed subtree to neutralise
1240
+ */
1241
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1242
+ const stack = [root];
1243
+ while (stack.length > 0) {
1244
+ const node = stack.pop();
1245
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1246
+ if (nodeType === NODE_TYPE.element) {
1247
+ _stripDisallowedAttributes(node);
1248
+ }
1249
+ const childNodes = getChildNodes(node);
1250
+ if (childNodes) {
1251
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1252
+ stack.push(childNodes[i]);
1253
+ }
1254
+ }
1255
+ }
1256
+ };
1257
+ /**
1258
+ * _neutralizePatchLinkage
1259
+ *
1260
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1261
+ * hardening, https://github.com/WICG/declarative-partial-updates).
1262
+ *
1263
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1264
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
1265
+ * reaches each node. On a live in-place root that leaves a window: from the
1266
+ * moment the root is connected until the walk arrives at a given node, that
1267
+ * node's linkage is live. A patch applied on connection/stream can fire as
1268
+ * a microtask during the walk and inject or teleport an unsanitized DOM
1269
+ * range into a region the iterator has already passed and will not revisit,
1270
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
1271
+ * whole tree once up front and sever every linkage before the walk begins,
1272
+ * closing that window.
1273
+ *
1274
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
1275
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1276
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1277
+ * to group with the rest of the declarative-partial-updates handling and
1278
+ * stay overridable, consistent with the codebase.
1279
+ *
1280
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1281
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1282
+ *
1283
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1284
+ * .html Q1): this mirrors the existing policy of keeping `for` on
1285
+ * <label>/<output>. If the shipping feature can drive a patch through a
1286
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1287
+ * attribute check at _isBasicCustomElement's caller must additionally drop
1288
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1289
+ *
1290
+ * @param root the in-place root to sweep
1291
+ */
1292
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1293
+ if (!SAFE_FOR_XML) {
1294
+ return;
1295
+ }
1296
+ const stack = [root];
1297
+ while (stack.length > 0) {
1298
+ const node = stack.pop();
1299
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1300
+ /* Remove range markers (the target side of a patch linkage): every
1301
+ processing instruction, and any markup-bearing comment. */
1302
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1303
+ try {
1304
+ remove(node);
1305
+ } catch (_) {
1306
+ /* Best-effort */
1307
+ }
1308
+ continue;
1309
+ }
1310
+ /* Strip patch-source attributes (the source side) off elements. */
1311
+ if (nodeType === NODE_TYPE.element) {
1312
+ const element = node;
1313
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1314
+ try {
1315
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1316
+ element.removeAttribute('patchsrc');
1317
+ }
1318
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
1319
+ element.removeAttribute('for');
1320
+ }
1321
+ } catch (_) {
1322
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
1323
+ }
1324
+ }
1325
+ const childNodes = getChildNodes(node);
1326
+ if (childNodes) {
1327
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1328
+ stack.push(childNodes[i]);
1329
+ }
1330
+ }
1331
+ }
1332
+ };
844
1333
  /**
845
1334
  * _initDocument
846
1335
  *
@@ -862,7 +1351,7 @@ var purify = {exports: {}};
862
1351
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
863
1352
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
864
1353
  }
865
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
1354
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
866
1355
  /*
867
1356
  * Use the DOMParser API by default, fallback later if needs be
868
1357
  * DOMParser not work for svg when has multiple root element.
@@ -898,33 +1387,297 @@ var purify = {exports: {}};
898
1387
  * @return The created NodeIterator
899
1388
  */
900
1389
  const _createNodeIterator = function _createNodeIterator(root) {
901
- return createNodeIterator.call(root.ownerDocument || root, root,
1390
+ /* Read ownerDocument through the cached Node.prototype getter, never the
1391
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
1392
+ clobbering child (<input name="ownerDocument"> or a form-associated
1393
+ external input) shadows the prototype getter and makes a direct read
1394
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
1395
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
1396
+ the walk's fail-closed barrier - leaving the caller's live tree, with
1397
+ any already-armed handler in it, un-neutralized. The cached getter
1398
+ returns the real Document regardless of the clobber. */
1399
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
1400
+ return createNodeIterator.call(doc || root, root,
902
1401
  // eslint-disable-next-line no-bitwise
903
1402
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
904
1403
  };
1404
+ /**
1405
+ * Replace template expression syntax (mustache, ERB, template
1406
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1407
+ * sites. Order matters: mustache, then ERB, then template literal.
1408
+ *
1409
+ * @param value the string to scrub
1410
+ * @returns the scrubbed string
1411
+ */
1412
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
1413
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
1414
+ value = stringReplace(value, ERB_EXPR$1, ' ');
1415
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
1416
+ return value;
1417
+ };
1418
+ /**
1419
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1420
+ * character data of an element subtree. Used as the final safety net for
1421
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
1422
+ * which only form after text-node normalization (e.g. fragments split across
1423
+ * stripped elements) cannot survive into a template-evaluating framework.
1424
+ *
1425
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
1426
+ * in place rather than round-tripping through innerHTML. This preserves
1427
+ * descendant node references (important for IN_PLACE callers), avoids a
1428
+ * serialize/reparse cycle, and reads literal character data — which means
1429
+ * `<%...%>` in text content matches the ERB regex against its real bytes
1430
+ * instead of the HTML-entity-escaped form innerHTML would produce.
1431
+ *
1432
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
1433
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
1434
+ *
1435
+ * @param node The root element whose character data should be scrubbed.
1436
+ */
1437
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1438
+ var _node$querySelectorAl;
1439
+ node.normalize();
1440
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
1441
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
1442
+ carry a form-named-getter override of ownerDocument. */
1443
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1444
+ const walker = createNodeIterator.call(doc || node, node,
1445
+ // eslint-disable-next-line no-bitwise
1446
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1447
+ let currentNode = walker.nextNode();
1448
+ while (currentNode) {
1449
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1450
+ currentNode = walker.nextNode();
1451
+ }
1452
+ // NodeIterator does not descend into <template>.content per the DOM spec,
1453
+ // so we must explicitly recurse into each template's content fragment,
1454
+ // mirroring the approach used by _sanitizeShadowDOM.
1455
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
1456
+ if (templates) {
1457
+ arrayForEach(templates, tmpl => {
1458
+ if (_isDocumentFragment(tmpl.content)) {
1459
+ _scrubTemplateExpressions2(tmpl.content);
1460
+ }
1461
+ });
1462
+ }
1463
+ };
905
1464
  /**
906
1465
  * _isClobbered
907
1466
  *
1467
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
1468
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
1469
+ * `name` attribute matching a prototype property shadows that property
1470
+ * on direct reads. We use this check at the IN_PLACE entry-point and
1471
+ * during attribute sanitization to refuse clobbered forms.
1472
+ *
908
1473
  * @param element element to check for clobbering attacks
909
1474
  * @return true if clobbered, false if safe
910
1475
  */
911
1476
  const _isClobbered = function _isClobbered(element) {
912
- 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');
1477
+ // Realm-independent tag-name probe. If we can't determine the tag
1478
+ // name at all, we can't reason about clobbering — return false
1479
+ // (the caller's other defences still apply).
1480
+ const realTagName = getNodeName ? getNodeName(element) : null;
1481
+ if (typeof realTagName !== 'string') {
1482
+ return false;
1483
+ }
1484
+ if (transformCaseFunc(realTagName) !== 'form') {
1485
+ return false;
1486
+ }
1487
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
1488
+ // Realm-safe NamedNodeMap detection: equality against the cached
1489
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
1490
+ // makes the direct read diverge from the cached read; a clean form
1491
+ // (same-realm OR foreign-realm) has both reads pointing at the same
1492
+ // canonical NamedNodeMap.
1493
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
1494
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
1495
+ // returns the integer 1 for any Element regardless of realm; direct
1496
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
1497
+ // the named child element. Cheap addition — nodeType is read from
1498
+ // an internal slot, no serialization cost — and removes a residual
1499
+ // clobbering surface used by several mXSS / PI / comment branches
1500
+ // in _sanitizeElements that compare currentNode.nodeType directly.
1501
+ element.nodeType !== getNodeType(element) ||
1502
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
1503
+ // "childNodes" shadows the prototype getter. Direct reads of
1504
+ // form.childNodes from a clobbered form return the named child
1505
+ // instead of the real NodeList, so any walk that reads it directly
1506
+ // skips the form's real children. Compare the direct read to the
1507
+ // cached Node.prototype getter — when the form's named-property
1508
+ // getter intercepts the read, the two values differ and we flag
1509
+ // the form. This catches every clobbering child type (input,
1510
+ // select, etc.) regardless of whether the named child happens to
1511
+ // carry a numeric .length, which a typeof-based probe would miss
1512
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
1513
+ element.childNodes !== getChildNodes(element);
913
1514
  };
914
1515
  /**
915
- * Checks whether the given object is a DOM node.
1516
+ * Checks whether the given value is a DocumentFragment from any realm.
1517
+ *
1518
+ * The realm-independent replacement reads `nodeType` through the cached
1519
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
1520
+ * constant (11). nodeType is a numeric value resolved from the node's
1521
+ * internal slot, identical across realms for the same kind of node.
1522
+ *
1523
+ * @param value object to check
1524
+ * @return true if value is a DocumentFragment-shaped node from any realm
1525
+ */
1526
+ const _isDocumentFragment = function _isDocumentFragment(value) {
1527
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1528
+ return false;
1529
+ }
1530
+ try {
1531
+ return getNodeType(value) === NODE_TYPE.documentFragment;
1532
+ } catch (_) {
1533
+ return false;
1534
+ }
1535
+ };
1536
+ /**
1537
+ * Checks whether the given object is a DOM node, including nodes that
1538
+ * originate from a different window/realm (e.g. an iframe's
1539
+ * contentDocument). The previous `value instanceof Node` check was
1540
+ * realm-bound: nodes from a different window failed it, causing
1541
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
1542
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
916
1543
  *
917
1544
  * @param value object to check whether it's a DOM node
918
- * @return true is object is a DOM node
1545
+ * @return true if value is a DOM node from any realm
919
1546
  */
920
1547
  const _isNode = function _isNode(value) {
921
- return typeof Node === 'function' && value instanceof Node;
1548
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1549
+ return false;
1550
+ }
1551
+ try {
1552
+ return typeof getNodeType(value) === 'number';
1553
+ } catch (_) {
1554
+ return false;
1555
+ }
922
1556
  };
923
1557
  function _executeHooks(hooks, currentNode, data) {
1558
+ if (hooks.length === 0) {
1559
+ return;
1560
+ }
924
1561
  arrayForEach(hooks, hook => {
925
1562
  hook.call(DOMPurify, currentNode, data, CONFIG);
926
1563
  });
927
1564
  }
1565
+ /**
1566
+ * Structural-threat checks that condemn a node regardless of the
1567
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1568
+ * processing instructions, markup-bearing comments. Pure predicate;
1569
+ * the caller removes. Check order is load-bearing.
1570
+ *
1571
+ * @param currentNode the node to inspect
1572
+ * @param tagName the node's transformCaseFunc'd tag name
1573
+ * @return true if the node must be removed
1574
+ */
1575
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1576
+ /* Detect mXSS attempts abusing namespace confusion */
1577
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
1578
+ return true;
1579
+ }
1580
+ /* Remove risky CSS construction leading to mXSS */
1581
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1582
+ return true;
1583
+ }
1584
+ /* Remove any occurrence of processing instructions */
1585
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1586
+ return true;
1587
+ }
1588
+ /* Remove any kind of possibly harmful comments */
1589
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
1590
+ return true;
1591
+ }
1592
+ return false;
1593
+ };
1594
+ /**
1595
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1596
+ * allowed custom elements (false return exits _sanitizeElements
1597
+ * early - the namespace and fallback-tag removal checks are
1598
+ * intentionally skipped for kept custom elements), else hoist
1599
+ * content per KEEP_CONTENT and remove.
1600
+ *
1601
+ * A kept custom element is the ONLY case in which this function
1602
+ * returns false, so the caller uses that return value to run the
1603
+ * afterSanitizeElements hook on the kept element and keep the
1604
+ * element-hook lifecycle consistent with normal allowlisted
1605
+ * elements (GHSA-c2j3-45gr-mqc4).
1606
+ *
1607
+ * @param currentNode the disallowed node
1608
+ * @param tagName the node's transformCaseFunc'd tag name
1609
+ * @return true if the node was removed, false if kept
1610
+ */
1611
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1612
+ /* Check if we have a custom element to handle */
1613
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1614
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1615
+ return false;
1616
+ }
1617
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1618
+ return false;
1619
+ }
1620
+ }
1621
+ /* Keep content except for bad-listed elements.
1622
+ Use the cached prototype getters exclusively — the previous code
1623
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1624
+ fallbacks, but the cached getters always return the canonical
1625
+ value (or null for a real parent-less node), so the fallback
1626
+ path was dead in safe cases and a clobbering surface in unsafe
1627
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
1628
+ parentNode)` check already gates correctly. */
1629
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1630
+ const parentNode = getParentNode(currentNode);
1631
+ const childNodes = getChildNodes(currentNode);
1632
+ if (childNodes && parentNode) {
1633
+ const childCount = childNodes.length;
1634
+ /* Hoist by moving each child up one level rather than deep-cloning
1635
+ it. Moving transfers every descendant exactly once, so a chain of
1636
+ nested disallowed elements costs O(n) instead of the O(n^2) that
1637
+ re-cloning the shrinking subtree at each level produced; it also
1638
+ empties the removed original, so `DOMPurify.removed` no longer
1639
+ pins whole subtrees. Moving preserves the in-place guarantee too:
1640
+ an original carrying already-queued resource events (`<img
1641
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
1642
+ relocated and sanitised rather than left detached but still armed.
1643
+ The sole case that must clone is removing the walk root itself.
1644
+ The result is serialised from the root's subtree, so a restrictive
1645
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
1646
+ leave the content inside it, which only cloning does. In IN_PLACE
1647
+ the root is pre-validated as an allowed tag and so is never removed
1648
+ here, so that path always takes the move branch.
1649
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1650
+ valid whether we move (drops the trailing entry) or clone (leaves
1651
+ the list intact). */
1652
+ for (let i = childCount - 1; i >= 0; --i) {
1653
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
1654
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1655
+ }
1656
+ }
1657
+ }
1658
+ _forceRemove(currentNode);
1659
+ return true;
1660
+ };
1661
+ /**
1662
+ * Fork a hook-mutable allowlist off its shared binding the first time a
1663
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
1664
+ * hook cannot widen the per-instance default or the setConfig binding by
1665
+ * reference and leak past the call. Returns the set unchanged once it is
1666
+ * already call-local, so repeated calls across elements are idempotent.
1667
+ *
1668
+ * @param hookList the uponSanitize* hook array for this event
1669
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
1670
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
1671
+ * @param setConfigSet the captured setConfig() binding, or null
1672
+ * @return a call-local clone if a hook is present and set is still shared,
1673
+ * else set unchanged
1674
+ */
1675
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
1676
+ if (hookList.length === 0) {
1677
+ return set;
1678
+ }
1679
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
1680
+ };
928
1681
  /**
929
1682
  * _sanitizeElements
930
1683
  *
@@ -934,86 +1687,112 @@ var purify = {exports: {}};
934
1687
  * @param currentNode to check for permission to exist
935
1688
  * @return true if node was killed, false if left alive
936
1689
  */
937
- const _sanitizeElements = function _sanitizeElements(currentNode) {
938
- let content = null;
1690
+ // eslint-disable-next-line complexity
1691
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
939
1692
  /* Execute a hook if present */
940
1693
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1694
+ /* A hook may have detached the node - treat it as removed (see the
1695
+ detached-node comment after the uponSanitizeElement hook below). On
1696
+ the IN_PLACE path, neutralize the detached subtree first so a queued
1697
+ resource handler on it cannot fire in page scope after we return. */
1698
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1699
+ if (IN_PLACE) {
1700
+ _neutralizeSubtree(currentNode);
1701
+ }
1702
+ return true;
1703
+ }
941
1704
  /* Check if element is clobbered or can clobber */
942
1705
  if (_isClobbered(currentNode)) {
943
1706
  _forceRemove(currentNode);
944
1707
  return true;
945
1708
  }
946
1709
  /* Now let's check the element's type and name */
947
- const tagName = transformCaseFunc(currentNode.nodeName);
1710
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1711
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
1712
+ hook may have been installed after that guard sampled the hook arrays
1713
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
1714
+ aliasing a shared binding that a widening hook would mutate by
1715
+ reference. Fork it before exposing it to the hook. */
1716
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
948
1717
  /* Execute a hook if present */
949
1718
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
950
1719
  tagName,
951
1720
  allowedTags: ALLOWED_TAGS
952
1721
  });
953
- /* Detect mXSS attempts abusing namespace confusion */
954
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
955
- _forceRemove(currentNode);
956
- return true;
957
- }
958
- /* Remove risky CSS construction leading to mXSS */
959
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
960
- _forceRemove(currentNode);
1722
+ /* A hook may have detached the node from the tree — a long-standing
1723
+ user pattern (issue #469; draw.io-style foreignObject filtering).
1724
+ Per the cached, unclobberable parentNode getter the node is
1725
+ genuinely out of the tree, so it can reach neither the serialized
1726
+ output nor an IN_PLACE live tree; treat it as removed and stop
1727
+ processing it. Without this guard, the unsafe-node / namespace
1728
+ checks below would call _forceRemove on a parentless node and hit
1729
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1730
+ wants gone but *cannot* detach (clobbered / parentless roots), the
1731
+ opposite of a node that is already safely gone. The walk root is
1732
+ exempt: a detached IN_PLACE root is legitimate input and must still
1733
+ be fully sanitized, and a kill-decision on it must keep hitting the
1734
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
1735
+ responsibility for placement: they are not recorded in
1736
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
1737
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
1738
+ can still hold a queued resource-event handler - e.g. an <img onload>
1739
+ that began loading when the caller built the live tree - which fires
1740
+ in page scope after sanitize returns even though the handler never
1741
+ reached the returned tree. That is the audit-5 F1 hazard, and the
1742
+ documented node.remove() hook pattern walks straight into it. So on
1743
+ the IN_PLACE path we neutralize the detached subtree inline here,
1744
+ stripping its non-allow-listed attributes before returning, exactly
1745
+ as the post-walk pass does for _forceRemove'd subtrees. */
1746
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1747
+ if (IN_PLACE) {
1748
+ _neutralizeSubtree(currentNode);
1749
+ }
961
1750
  return true;
962
1751
  }
963
- /* Remove any occurrence of processing instructions */
964
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
965
- _forceRemove(currentNode);
966
- return true;
967
- }
968
- /* Remove any kind of possibly harmful comments */
969
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
1752
+ /* Remove mXSS vectors, processing instructions and risky comments */
1753
+ if (_isUnsafeNode(currentNode, tagName)) {
970
1754
  _forceRemove(currentNode);
971
1755
  return true;
972
1756
  }
973
1757
  /* Remove element if anything forbids its presence */
974
1758
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
975
- /* Check if we have a custom element to handle */
976
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
977
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
978
- return false;
979
- }
980
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
981
- return false;
982
- }
983
- }
984
- /* Keep content except for bad-listed elements */
985
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
986
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
987
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
988
- if (childNodes && parentNode) {
989
- const childCount = childNodes.length;
990
- for (let i = childCount - 1; i >= 0; --i) {
991
- const childClone = cloneNode(childNodes[i], true);
992
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
993
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
994
- }
995
- }
1759
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
1760
+ /* A false return means the node is a custom element kept via
1761
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
1762
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
1763
+ element-hook lifecycle matches normal allowlisted elements: a
1764
+ security policy applied in this hook (e.g. stripping an attribute
1765
+ from every surviving element) must not silently skip kept custom
1766
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
1767
+ tail below - the hook runs, then the walker's subsequent
1768
+ _sanitizeAttributes pass sanitizes the element's attributes. The
1769
+ deliberately skipped namespace and fallback-tag removal checks stay
1770
+ skipped; they are removal decisions, not the hook contract. */
1771
+ if (removed === false) {
1772
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
996
1773
  }
997
- _forceRemove(currentNode);
998
- return true;
999
- }
1000
- /* Check whether element has a valid namespace */
1001
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
1774
+ return removed;
1775
+ }
1776
+ /* Check whether element has a valid namespace.
1777
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1778
+ nodeType getter rather than `instanceof Element`, which is realm-
1779
+ bound and short-circuits to false for any node minted in a different
1780
+ realm — letting a foreign-realm element with a forbidden namespace
1781
+ slip past the namespace check entirely. */
1782
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
1783
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
1002
1784
  _forceRemove(currentNode);
1003
1785
  return true;
1004
1786
  }
1005
1787
  /* Make sure that older browsers don't get fallback-tag mXSS */
1006
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
1788
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
1007
1789
  _forceRemove(currentNode);
1008
1790
  return true;
1009
1791
  }
1010
1792
  /* Sanitize element content to be template-safe */
1011
1793
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1012
1794
  /* Get the element's text content */
1013
- content = currentNode.textContent;
1014
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1015
- content = stringReplace(content, expr, ' ');
1016
- });
1795
+ const content = _stripTemplateExpressions(currentNode.textContent);
1017
1796
  if (currentNode.textContent !== content) {
1018
1797
  arrayPush(DOMPurify.removed, {
1019
1798
  element: currentNode.cloneNode()
@@ -1039,15 +1818,43 @@ var purify = {exports: {}};
1039
1818
  if (FORBID_ATTR[lcName]) {
1040
1819
  return false;
1041
1820
  }
1821
+ /* Reject declarative-partial-updates patch-linkage attributes
1822
+ (https://github.com/WICG/declarative-partial-updates).
1823
+ Empirical note (Chrome 150, verified — see
1824
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
1825
+ sanitization. For the string path it fires during sanitize()'s own
1826
+ parse, so the walk sees and sanitizes the fully materialized expanded
1827
+ tree — teleports into MathML/SVG integration points included; a
1828
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
1829
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
1830
+ Either way DOMPurify is NOT blind to the patch.
1831
+ This removal is therefore defense-in-depth rather than the sole barrier:
1832
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
1833
+ in the caller's context, and keeps behaviour deterministic if a future
1834
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
1835
+ anywhere else (notably <template for>) it links the element to a patch
1836
+ target and teleports or removes an arbitrary DOM range by id/marker name.
1837
+ `patchsrc` fetches remote markup and is treated as a script-loading
1838
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
1839
+ other structural-threat checks and stays overridable, consistent with
1840
+ the rest of the codebase. PI range markers are already removed by
1841
+ _isUnsafeNode. */
1842
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
1843
+ return false;
1844
+ }
1845
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
1846
+ return false;
1847
+ }
1042
1848
  /* Make sure attribute cannot clobber */
1043
1849
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
1044
1850
  return false;
1045
1851
  }
1852
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
1046
1853
  /* Allow valid data-* attributes: At least one character after "-"
1047
1854
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1048
1855
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1049
1856
  We don't need to check the value; it's always URI safe. */
1050
- 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]) {
1857
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
1051
1858
  if (
1052
1859
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1053
1860
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1059,11 +1866,15 @@ var purify = {exports: {}};
1059
1866
  return false;
1060
1867
  }
1061
1868
  /* Check value is safe. First, is attr inert? If so, is safe */
1062
- } 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) {
1869
+ } 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) {
1063
1870
  return false;
1064
1871
  } else ;
1065
1872
  return true;
1066
1873
  };
1874
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
1875
+ * never be treated as basic custom elements even when a permissive
1876
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
1877
+ 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']);
1067
1878
  /**
1068
1879
  * _isBasicCustomElement
1069
1880
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -1073,7 +1884,64 @@ var purify = {exports: {}};
1073
1884
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1074
1885
  */
1075
1886
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1076
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
1887
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1888
+ };
1889
+ /**
1890
+ * Wrap an attribute value in the matching Trusted Types object when
1891
+ * the active policy requires it. Namespaced attributes pass through
1892
+ * unchanged (no TT support yet, see
1893
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1894
+ *
1895
+ * @param lcTag lowercase tag name of the containing element
1896
+ * @param lcName lowercase attribute name
1897
+ * @param namespaceURI the attribute's namespace, if any
1898
+ * @param value the attribute value to wrap
1899
+ * @return the value, wrapped when Trusted Types demand it
1900
+ */
1901
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
1902
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
1903
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1904
+ case 'TrustedHTML':
1905
+ {
1906
+ return _createTrustedHTML(value);
1907
+ }
1908
+ case 'TrustedScriptURL':
1909
+ {
1910
+ return _createTrustedScriptURL(value);
1911
+ }
1912
+ }
1913
+ }
1914
+ return value;
1915
+ };
1916
+ /**
1917
+ * Write a modified attribute value back onto the element. On
1918
+ * success, re-probe for clobbering introduced by the new value and
1919
+ * remove the element when found; otherwise pop the removal entry
1920
+ * recorded by the earlier _removeAttribute (long-standing pairing
1921
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
1922
+ * failure, remove the attribute instead.
1923
+ *
1924
+ * @param currentNode the element carrying the attribute
1925
+ * @param name the attribute name as present on the element
1926
+ * @param namespaceURI the attribute's namespace, if any
1927
+ * @param value the new attribute value
1928
+ */
1929
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
1930
+ try {
1931
+ if (namespaceURI) {
1932
+ currentNode.setAttributeNS(namespaceURI, name, value);
1933
+ } else {
1934
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1935
+ currentNode.setAttribute(name, value);
1936
+ }
1937
+ if (_isClobbered(currentNode)) {
1938
+ _forceRemove(currentNode);
1939
+ } else {
1940
+ arrayPop(DOMPurify.removed);
1941
+ }
1942
+ } catch (_) {
1943
+ _removeAttribute(name, currentNode);
1944
+ }
1077
1945
  };
1078
1946
  /**
1079
1947
  * _sanitizeAttributes
@@ -1088,13 +1956,14 @@ var purify = {exports: {}};
1088
1956
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1089
1957
  /* Execute a hook if present */
1090
1958
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1091
- const {
1092
- attributes
1093
- } = currentNode;
1959
+ const attributes = currentNode.attributes;
1094
1960
  /* Check if we have attributes; if not we might have a text node */
1095
1961
  if (!attributes || _isClobbered(currentNode)) {
1096
1962
  return;
1097
1963
  }
1964
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
1965
+ attribute allowlist off its shared binding before a hook can see it. */
1966
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
1098
1967
  const hookEvent = {
1099
1968
  attrName: '',
1100
1969
  attrValue: '',
@@ -1103,14 +1972,13 @@ var purify = {exports: {}};
1103
1972
  forceKeepAttr: undefined
1104
1973
  };
1105
1974
  let l = attributes.length;
1975
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1106
1976
  /* Go backwards over all attributes; safely remove bad ones */
1107
1977
  while (l--) {
1108
1978
  const attr = attributes[l];
1109
- const {
1110
- name,
1111
- namespaceURI,
1112
- value: attrValue
1113
- } = attr;
1979
+ const name = attr.name,
1980
+ namespaceURI = attr.namespaceURI,
1981
+ attrValue = attr.value;
1114
1982
  const lcName = transformCaseFunc(name);
1115
1983
  const initValue = attrValue;
1116
1984
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1124,12 +1992,14 @@ var purify = {exports: {}};
1124
1992
  /* Full DOM Clobbering protection via namespace isolation,
1125
1993
  * Prefix id and name attributes with `user-content-`
1126
1994
  */
1127
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
1995
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
1128
1996
  // Remove the attribute with this value
1129
1997
  _removeAttribute(name, currentNode);
1130
1998
  // Prefix the value and later re-create the attribute with the sanitized value
1131
1999
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
1132
2000
  }
2001
+ // Else: already prefixed, leave the attribute alone — the prefix is
2002
+ // itself the clobbering protection, and re-applying it is incorrect.
1133
2003
  /* Work around a security issue with comments inside attributes */
1134
2004
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
1135
2005
  _removeAttribute(name, currentNode);
@@ -1140,7 +2010,7 @@ var purify = {exports: {}};
1140
2010
  _removeAttribute(name, currentNode);
1141
2011
  continue;
1142
2012
  }
1143
- /* Did the hooks approve of the attribute? */
2013
+ /* Did the hooks force-keep the attribute? */
1144
2014
  if (hookEvent.forceKeepAttr) {
1145
2015
  continue;
1146
2016
  }
@@ -1150,56 +2020,24 @@ var purify = {exports: {}};
1150
2020
  continue;
1151
2021
  }
1152
2022
  /* Work around a security issue in jQuery 3.0 */
1153
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
2023
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
1154
2024
  _removeAttribute(name, currentNode);
1155
2025
  continue;
1156
2026
  }
1157
2027
  /* Sanitize attribute content to be template-safe */
1158
2028
  if (SAFE_FOR_TEMPLATES) {
1159
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1160
- value = stringReplace(value, expr, ' ');
1161
- });
2029
+ value = _stripTemplateExpressions(value);
1162
2030
  }
1163
2031
  /* Is `value` valid for this attribute? */
1164
- const lcTag = transformCaseFunc(currentNode.nodeName);
1165
2032
  if (!_isValidAttribute(lcTag, lcName, value)) {
1166
2033
  _removeAttribute(name, currentNode);
1167
2034
  continue;
1168
2035
  }
1169
2036
  /* Handle attributes that require Trusted Types */
1170
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
1171
- if (namespaceURI) ; else {
1172
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1173
- case 'TrustedHTML':
1174
- {
1175
- value = trustedTypesPolicy.createHTML(value);
1176
- break;
1177
- }
1178
- case 'TrustedScriptURL':
1179
- {
1180
- value = trustedTypesPolicy.createScriptURL(value);
1181
- break;
1182
- }
1183
- }
1184
- }
1185
- }
2037
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1186
2038
  /* Handle invalid data-* attribute set by try-catching it */
1187
2039
  if (value !== initValue) {
1188
- try {
1189
- if (namespaceURI) {
1190
- currentNode.setAttributeNS(namespaceURI, name, value);
1191
- } else {
1192
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1193
- currentNode.setAttribute(name, value);
1194
- }
1195
- if (_isClobbered(currentNode)) {
1196
- _forceRemove(currentNode);
1197
- } else {
1198
- arrayPop(DOMPurify.removed);
1199
- }
1200
- } catch (_) {
1201
- _removeAttribute(name, currentNode);
1202
- }
2040
+ _setAttributeValue(currentNode, name, namespaceURI, value);
1203
2041
  }
1204
2042
  }
1205
2043
  /* Execute a hook if present */
@@ -1219,17 +2057,136 @@ var purify = {exports: {}};
1219
2057
  /* Execute a hook if present */
1220
2058
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
1221
2059
  /* Sanitize tags and elements */
1222
- _sanitizeElements(shadowNode);
2060
+ _sanitizeElements(shadowNode, fragment);
1223
2061
  /* Check attributes next */
1224
2062
  _sanitizeAttributes(shadowNode);
1225
- /* Deep shadow DOM detected */
1226
- if (shadowNode.content instanceof DocumentFragment) {
2063
+ /* Deep shadow DOM detected.
2064
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
2065
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
2066
+ recurse into <template>.content from foreign realms too. */
2067
+ if (_isDocumentFragment(shadowNode.content)) {
1227
2068
  _sanitizeShadowDOM2(shadowNode.content);
1228
2069
  }
2070
+ /* An element iterated here may itself host an attached
2071
+ shadow root. The default NodeIterator does not enter shadow
2072
+ trees, so a shadow root nested inside template.content was
2073
+ previously reached by no walk at all (the pre-pass at
2074
+ _sanitizeAttachedShadowRoots descends via childNodes, which
2075
+ doesn't enter template.content; the template-content recursion
2076
+ above iterates the content but never inspected shadowRoot).
2077
+ Walk it explicitly. The nodeType guard avoids reading
2078
+ shadowRoot off text / comment / CDATA / PI nodes that the
2079
+ iterator also surfaces. */
2080
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
2081
+ if (shadowNodeType === NODE_TYPE.element) {
2082
+ const innerSr = getShadowRoot(shadowNode);
2083
+ if (_isDocumentFragment(innerSr)) {
2084
+ _sanitizeAttachedShadowRoots(innerSr);
2085
+ _sanitizeShadowDOM2(innerSr);
2086
+ }
2087
+ }
1229
2088
  }
1230
2089
  /* Execute a hook if present */
1231
2090
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1232
2091
  };
2092
+ /**
2093
+ * _sanitizeAttachedShadowRoots
2094
+ *
2095
+ * Walks `root` and feeds every attached shadow root we encounter into
2096
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
2097
+ * does not descend into shadow trees, so nodes inside an attached
2098
+ * shadow root would otherwise be skipped entirely.
2099
+ *
2100
+ * Two real input paths put attached shadow roots in front of us:
2101
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
2102
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
2103
+ * shadow root because it was created with `clonable: true`.
2104
+ *
2105
+ * This pass runs once, up front, so the main iteration loop (and the
2106
+ * existing _sanitizeShadowDOM template-content recursion) stay
2107
+ * untouched — string-input paths are not affected.
2108
+ *
2109
+ * @param root the subtree root to walk for attached shadow roots
2110
+ */
2111
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
2112
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
2113
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
2114
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
2115
+ call-stack budget would otherwise overflow native recursion here and
2116
+ throw at the IN_PLACE entry pre-pass, before a single node is
2117
+ sanitized, leaving the caller's live tree untouched (fail-open). See
2118
+ campaign-3 F4. A heap stack keeps depth off the call stack.
2119
+ Each work item is either a node to descend into, or a deferred
2120
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
2121
+ form preserves the original post-order discipline: a shadow root's
2122
+ nested shadow roots are discovered before the outer shadow is
2123
+ sanitized (which may remove hosts). Pushes are in reverse of the
2124
+ desired processing order (LIFO): template content, then children, then
2125
+ the shadow-sanitize, then the shadow walk — so the order matches the
2126
+ previous recursion exactly. */
2127
+ const stack = [{
2128
+ node: root,
2129
+ shadow: null
2130
+ }];
2131
+ while (stack.length > 0) {
2132
+ const item = stack.pop();
2133
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
2134
+ if (item.shadow) {
2135
+ _sanitizeShadowDOM2(item.shadow);
2136
+ continue;
2137
+ }
2138
+ const node = item.node;
2139
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
2140
+ const isElement = nodeType === NODE_TYPE.element;
2141
+ /* (pushed last → processed first) Children, snapshotted in reverse so
2142
+ the first child is processed first. Snapshotting matters because a
2143
+ hook may detach siblings mid-walk. */
2144
+ const childNodes = getChildNodes(node);
2145
+ if (childNodes) {
2146
+ for (let i = childNodes.length - 1; i >= 0; --i) {
2147
+ stack.push({
2148
+ node: childNodes[i],
2149
+ shadow: null
2150
+ });
2151
+ }
2152
+ }
2153
+ /* (pushed before children → processed after them, matching the old
2154
+ "template content last" order) When the node is a <template>,
2155
+ descend into its content. */
2156
+ if (isElement) {
2157
+ const rootName = getNodeName ? getNodeName(node) : null;
2158
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
2159
+ const content = node.content;
2160
+ if (_isDocumentFragment(content)) {
2161
+ stack.push({
2162
+ node: content,
2163
+ shadow: null
2164
+ });
2165
+ }
2166
+ }
2167
+ }
2168
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
2169
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
2170
+ rather than `instanceof DocumentFragment`, which is realm-bound and
2171
+ silently skipped foreign-realm shadow roots (e.g.
2172
+ iframe.contentDocument attachShadow). */
2173
+ if (isElement) {
2174
+ const sr = getShadowRoot(node);
2175
+ if (_isDocumentFragment(sr)) {
2176
+ /* Push the deferred sanitise first so it pops after the shadow
2177
+ walk we push next, i.e. nested shadow roots are discovered
2178
+ before this one is sanitised. */
2179
+ stack.push({
2180
+ node: null,
2181
+ shadow: sr
2182
+ }, {
2183
+ node: sr,
2184
+ shadow: null
2185
+ });
2186
+ }
2187
+ }
2188
+ }
2189
+ };
1233
2190
  // eslint-disable-next-line complexity
1234
2191
  DOMPurify.sanitize = function (dirty) {
1235
2192
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1246,13 +2203,9 @@ var purify = {exports: {}};
1246
2203
  }
1247
2204
  /* Stringify, in case dirty is an object */
1248
2205
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
1249
- if (typeof dirty.toString === 'function') {
1250
- dirty = dirty.toString();
1251
- if (typeof dirty !== 'string') {
1252
- throw typeErrorCreate('dirty is not a string, aborting');
1253
- }
1254
- } else {
1255
- throw typeErrorCreate('toString is not a function');
2206
+ dirty = stringifyValue(dirty);
2207
+ if (typeof dirty !== 'string') {
2208
+ throw typeErrorCreate('dirty is not a string, aborting');
1256
2209
  }
1257
2210
  }
1258
2211
  /* Return dirty HTML if DOMPurify cannot run */
@@ -1260,24 +2213,90 @@ var purify = {exports: {}};
1260
2213
  return dirty;
1261
2214
  }
1262
2215
  /* Assign config vars */
1263
- if (!SET_CONFIG) {
2216
+ if (SET_CONFIG) {
2217
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
2218
+ * not re-derived per call. Restore them from the pristine bindings
2219
+ * captured at setConfig() time so a previous call's hook clone (mutated
2220
+ * below) does not carry over. */
2221
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
2222
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
2223
+ } else {
1264
2224
  _parseConfig(cfg);
1265
2225
  }
2226
+ /* Clone the hook-mutable allowlists before the walk whenever an
2227
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2228
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2229
+ * a hook that widens them would otherwise mutate the shared set
2230
+ * permanently: across later calls and across every element. Cloning per
2231
+ * walk keeps documented in-call widening working while scoping it to the
2232
+ * call. A single guard for both config paths - the per-call path rebinds
2233
+ * the sets in _parseConfig each call, the persistent path restores them
2234
+ * from the captured bindings just above - so the two cannot diverge. */
2235
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
2236
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2237
+ }
2238
+ if (hooks.uponSanitizeAttribute.length > 0) {
2239
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2240
+ }
1266
2241
  /* Clean up removed elements */
1267
2242
  DOMPurify.removed = [];
1268
- /* Check if dirty is correctly typed for IN_PLACE */
1269
- if (typeof dirty === 'string') {
1270
- IN_PLACE = false;
1271
- }
1272
- if (IN_PLACE) {
1273
- /* Do some early pre-sanitization to avoid unsafe root nodes */
1274
- if (dirty.nodeName) {
1275
- const tagName = transformCaseFunc(dirty.nodeName);
2243
+ /* Resolve IN_PLACE for this call without mutating persistent config.
2244
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
2245
+ where _parseConfig is skipped on later calls: a single string call would
2246
+ disable in-place mode for every subsequent node call, returning a
2247
+ sanitized copy while leaving the caller's node — which in-place callers
2248
+ keep using and whose return value they ignore — unsanitized. REPORT-2. */
2249
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2250
+ if (inPlace) {
2251
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
2252
+ linkage across the live tree BEFORE the walk, so no patch can fire
2253
+ mid-walk and inject into an already-processed region. Runs first, so
2254
+ it also covers the forbidden/clobbered roots that throw below. */
2255
+ _neutralizePatchLinkage(dirty);
2256
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
2257
+ Read nodeName through the cached prototype getter — a clobbering
2258
+ child named "nodeName" on the form root would otherwise shadow
2259
+ the property and let this check skip the root-allowlist
2260
+ validation entirely. */
2261
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
2262
+ if (typeof nn === 'string') {
2263
+ const tagName = transformCaseFunc(nn);
1276
2264
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
2265
+ /* Fail closed on a live root: neutralize handlers/children before
2266
+ throwing, exactly as the mid-walk abort path does. */
2267
+ _neutralizeRoot(dirty);
1277
2268
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1278
2269
  }
1279
2270
  }
1280
- } else if (dirty instanceof Node) {
2271
+ /* Pre-flight the root through _isClobbered. The iterator-driven
2272
+ removal path can not detach a parent-less root: _forceRemove
2273
+ falls through to Element.prototype.remove(), which per spec
2274
+ is a no-op on a node with no parent. A clobbered root would
2275
+ then survive the main loop with its attributes uninspected,
2276
+ because _sanitizeAttributes early-returns on _isClobbered. The
2277
+ result would be an attacker-controlled form, complete with any
2278
+ event-handler attributes the caller passed in, handed back to
2279
+ the application unsanitized. Refuse to sanitize such a root
2280
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2281
+ if (_isClobbered(dirty)) {
2282
+ /* Fail closed on a live clobbered root before throwing.
2283
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
2284
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
2285
+ _neutralizeRoot(dirty);
2286
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
2287
+ }
2288
+ /* Sanitize attached shadow roots before the main iterator runs.
2289
+ The iterator does not descend into shadow trees. Same fail-closed
2290
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
2291
+ inside a shadow root could abort this pre-pass before the walk runs,
2292
+ which would otherwise leave the entire live tree unsanitized. */
2293
+ try {
2294
+ _sanitizeAttachedShadowRoots(dirty);
2295
+ } catch (error) {
2296
+ _neutralizeRoot(dirty);
2297
+ throw error;
2298
+ }
2299
+ } else if (_isNode(dirty)) {
1281
2300
  /* If dirty is a DOM element, append to an empty document to avoid
1282
2301
  elements being stripped by the parser */
1283
2302
  body = _initDocument('<!---->');
@@ -1291,12 +2310,18 @@ var purify = {exports: {}};
1291
2310
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1292
2311
  body.appendChild(importedNode);
1293
2312
  }
2313
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
2314
+ them before the main iterator runs, since the iterator does not
2315
+ descend into shadow trees. The walk routes every read through a
2316
+ cached prototype getter so clobbering descendants on a form root
2317
+ cannot hide a shadow host from this pass. */
2318
+ _sanitizeAttachedShadowRoots(importedNode);
1294
2319
  } else {
1295
2320
  /* Exit directly if we have nothing to do */
1296
2321
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
1297
2322
  // eslint-disable-next-line unicorn/prefer-includes
1298
2323
  dirty.indexOf('<') === -1) {
1299
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
2324
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
1300
2325
  }
1301
2326
  /* Initialize the document to work on */
1302
2327
  body = _initDocument(dirty);
@@ -1310,31 +2335,71 @@ var purify = {exports: {}};
1310
2335
  _forceRemove(body.firstChild);
1311
2336
  }
1312
2337
  /* Get node iterator */
1313
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
1314
- /* Now start iterating over the created document */
1315
- while (currentNode = nodeIterator.nextNode()) {
1316
- /* Sanitize tags and elements */
1317
- _sanitizeElements(currentNode);
1318
- /* Check attributes next */
1319
- _sanitizeAttributes(currentNode);
1320
- /* Shadow DOM detected, sanitize it */
1321
- if (currentNode.content instanceof DocumentFragment) {
1322
- _sanitizeShadowDOM2(currentNode.content);
2338
+ const walkRoot = inPlace ? dirty : body;
2339
+ /* Now start iterating over the created document.
2340
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2341
+ engine/custom-element mutation can detach a node mid-walk so
2342
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2343
+ barrier the caller's in-place tree would be left half-sanitized with the
2344
+ unvisited tail still armed. _createNodeIterator itself is inside the
2345
+ barrier too: constructing the iterator dereferences the root's document,
2346
+ and any failure there (e.g. an exotic/clobbered root) must still fail
2347
+ closed rather than skip the neutralize. On any throw we fail closed -
2348
+ strip the in-place root bare - then rethrow so the existing throw
2349
+ contract is preserved. (String/DOM-copy paths never return the partial
2350
+ body, so the propagating throw is already fail-closed there.) */
2351
+ try {
2352
+ const nodeIterator = _createNodeIterator(walkRoot);
2353
+ while (currentNode = nodeIterator.nextNode()) {
2354
+ /* Sanitize tags and elements */
2355
+ _sanitizeElements(currentNode, walkRoot);
2356
+ /* Check attributes next */
2357
+ _sanitizeAttributes(currentNode);
2358
+ /* Shadow DOM detected, sanitize it.
2359
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
2360
+ instead of instanceof, so foreign-realm <template>.content is
2361
+ walked correctly. */
2362
+ if (_isDocumentFragment(currentNode.content)) {
2363
+ _sanitizeShadowDOM2(currentNode.content);
2364
+ }
1323
2365
  }
2366
+ } catch (error) {
2367
+ if (inPlace) {
2368
+ _neutralizeRoot(dirty);
2369
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
2370
+ detached from the root, so _neutralizeRoot's subtree pass does not
2371
+ reach them. Defuse them too, mirroring the success-path loop below. */
2372
+ arrayForEach(DOMPurify.removed, entry => {
2373
+ if (entry.element) {
2374
+ _neutralizeSubtree(entry.element);
2375
+ }
2376
+ });
2377
+ }
2378
+ throw error;
1324
2379
  }
1325
2380
  /* If we sanitized `dirty` in-place, return it. */
1326
- if (IN_PLACE) {
2381
+ if (inPlace) {
2382
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
2383
+ the caller's live tree is detached but may still hold a queued
2384
+ resource-event handler that fires in page scope after we return. The
2385
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
2386
+ non-allow-listed attributes off every other removed subtree (clobber,
2387
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
2388
+ cancelled before any event can fire. Runs synchronously, pre-return. */
2389
+ arrayForEach(DOMPurify.removed, entry => {
2390
+ if (entry.element) {
2391
+ _neutralizeSubtree(entry.element);
2392
+ }
2393
+ });
2394
+ if (SAFE_FOR_TEMPLATES) {
2395
+ _scrubTemplateExpressions2(dirty);
2396
+ }
1327
2397
  return dirty;
1328
2398
  }
1329
2399
  /* Return sanitized string or DOM */
1330
2400
  if (RETURN_DOM) {
1331
2401
  if (SAFE_FOR_TEMPLATES) {
1332
- body.normalize();
1333
- let html = body.innerHTML;
1334
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1335
- html = stringReplace(html, expr, ' ');
1336
- });
1337
- body.innerHTML = html;
2402
+ _scrubTemplateExpressions2(body);
1338
2403
  }
1339
2404
  if (RETURN_DOM_FRAGMENT) {
1340
2405
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1364,20 +2429,28 @@ var purify = {exports: {}};
1364
2429
  }
1365
2430
  /* Sanitize final string template-safe */
1366
2431
  if (SAFE_FOR_TEMPLATES) {
1367
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1368
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
1369
- });
2432
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
1370
2433
  }
1371
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
2434
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
1372
2435
  };
1373
2436
  DOMPurify.setConfig = function () {
1374
2437
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1375
2438
  _parseConfig(cfg);
1376
2439
  SET_CONFIG = true;
2440
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2441
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
1377
2442
  };
1378
2443
  DOMPurify.clearConfig = function () {
1379
2444
  CONFIG = null;
1380
2445
  SET_CONFIG = false;
2446
+ SET_CONFIG_ALLOWED_TAGS = null;
2447
+ SET_CONFIG_ALLOWED_ATTR = null;
2448
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
2449
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
2450
+ // never recreated — Trusted Types throws on duplicate names) is restored by
2451
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
2452
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
2453
+ emptyHTML = '';
1381
2454
  };
1382
2455
  DOMPurify.isValidAttribute = function (tag, attr, value) {
1383
2456
  /* Initialize shared config vars if necessary. */
@@ -1392,9 +2465,19 @@ var purify = {exports: {}};
1392
2465
  if (typeof hookFunction !== 'function') {
1393
2466
  return;
1394
2467
  }
2468
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2469
+ * '__proto__') indexes off the prototype chain rather than a real
2470
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2471
+ * with an own-property check against the known hook names. */
2472
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2473
+ return;
2474
+ }
1395
2475
  arrayPush(hooks[entryPoint], hookFunction);
1396
2476
  };
1397
2477
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
2478
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2479
+ return undefined;
2480
+ }
1398
2481
  if (hookFunction !== undefined) {
1399
2482
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
1400
2483
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -1402,6 +2485,9 @@ var purify = {exports: {}};
1402
2485
  return arrayPop(hooks[entryPoint]);
1403
2486
  };
1404
2487
  DOMPurify.removeHooks = function (entryPoint) {
2488
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2489
+ return;
2490
+ }
1405
2491
  hooks[entryPoint] = [];
1406
2492
  };
1407
2493
  DOMPurify.removeAllHooks = function () {