@touchvue/chat 1.0.0-beta.56 → 1.0.0-beta.57

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 (121) hide show
  1. package/es/index.mjs.map +1 -1
  2. package/es/node_modules/.pnpm/{dompurify@3.4.3 → dompurify@3.4.11}/node_modules/dompurify/dist/purify.es.mjs +939 -226
  3. package/es/node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.mjs.map +1 -0
  4. package/es/node_modules/.pnpm/{linkify-it@5.0.0 → linkify-it@5.0.2}/node_modules/linkify-it/index.mjs +109 -105
  5. package/es/node_modules/.pnpm/linkify-it@5.0.2/node_modules/linkify-it/index.mjs.map +1 -0
  6. package/es/node_modules/.pnpm/{linkify-it@5.0.0 → linkify-it@5.0.2}/node_modules/linkify-it/lib/re.mjs +34 -30
  7. package/es/node_modules/.pnpm/linkify-it@5.0.2/node_modules/linkify-it/lib/re.mjs.map +1 -0
  8. package/es/node_modules/.pnpm/{markdown-it-deflist@3.0.0 → markdown-it-deflist@3.0.1}/node_modules/markdown-it-deflist/index.mjs +2 -1
  9. package/es/node_modules/.pnpm/markdown-it-deflist@3.0.1/node_modules/markdown-it-deflist/index.mjs.map +1 -0
  10. package/es/node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/index.mjs +1 -1
  11. package/es/package.json.mjs +1 -1
  12. package/es/packages/components/touchchat/component/AiRobot/face.vue2.mjs.map +1 -1
  13. package/es/packages/components/touchchat/component/AiRobot/letter.vue2.mjs +1 -1
  14. package/es/packages/components/touchchat/component/AiRobot/letter.vue2.mjs.map +1 -1
  15. package/es/packages/components/touchchat/component/AiRobot/meterialPrepare.vue2.mjs.map +1 -1
  16. package/es/packages/components/touchchat/component/AiRobot/start.vue2.mjs.map +1 -1
  17. package/es/packages/components/touchchat/component/CardView.vue2.mjs.map +1 -1
  18. package/es/packages/components/touchchat/component/ImageFile.vue2.mjs.map +1 -1
  19. package/es/packages/components/touchchat/component/ImageView.vue2.mjs.map +1 -1
  20. package/es/packages/components/touchchat/component/LinkView.vue2.mjs.map +1 -1
  21. package/es/packages/components/touchchat/component/MarkLayer.vue2.mjs.map +1 -1
  22. package/es/packages/components/touchchat/component/ModuleSelect.vue2.mjs.map +1 -1
  23. package/es/packages/components/touchchat/component/Screenshot.vue2.mjs.map +1 -1
  24. package/es/packages/components/touchchat/component/ThoughtChain.vue2.mjs.map +1 -1
  25. package/es/packages/components/touchchat/component/UploadView.vue2.mjs.map +1 -1
  26. package/es/packages/components/touchchat/component/UploadView_api.vue2.mjs.map +1 -1
  27. package/es/packages/components/touchchat/index.mjs.map +1 -1
  28. package/es/packages/components/touchchat/src/AiChat/Chat/TouchChat.mjs.map +1 -1
  29. package/es/packages/components/touchchat/src/AiChat/Chat/useChat.mjs +7 -2
  30. package/es/packages/components/touchchat/src/AiChat/Chat/useChat.mjs.map +1 -1
  31. package/es/packages/components/touchchat/src/AiChat/Chat/useMessageRender.mjs +1 -1
  32. package/es/packages/components/touchchat/src/AiChat/Chat/useSSE.mjs +10 -7
  33. package/es/packages/components/touchchat/src/AiChat/Chat/useSSE.mjs.map +1 -1
  34. package/es/packages/components/touchchat/src/AiChat/Chat/useScroll.mjs.map +1 -1
  35. package/es/packages/components/touchchat/src/AiChat/ChatInput.vue2.mjs +6 -2
  36. package/es/packages/components/touchchat/src/AiChat/ChatInput.vue2.mjs.map +1 -1
  37. package/es/packages/components/touchchat/src/AiChat/HistoryList.vue2.mjs.map +1 -1
  38. package/es/packages/components/touchchat/src/AiChat/HistorySidebar.vue2.mjs.map +1 -1
  39. package/es/packages/components/touchchat/src/AiChat/SiderBarView.vue2.mjs.map +1 -1
  40. package/es/packages/components/touchchat/src/AiChat/TouchAgent.vue2.mjs +136 -120
  41. package/es/packages/components/touchchat/src/AiChat/TouchAgent.vue2.mjs.map +1 -1
  42. package/es/packages/components/touchchat/src/index.vue2.mjs +6 -1
  43. package/es/packages/components/touchchat/src/index.vue2.mjs.map +1 -1
  44. package/es/packages/components/touchchat/src/types/a2a.mjs.map +1 -1
  45. package/es/packages/components/touchchat/utils/a2aService.mjs +189 -132
  46. package/es/packages/components/touchchat/utils/a2aService.mjs.map +1 -1
  47. package/es/packages/components/touchchat/utils/chatRobot.mjs.map +1 -1
  48. package/es/packages/components/touchchat/utils/deepCloneDom.mjs.map +1 -1
  49. package/es/packages/components/touchchat/utils/hight-it.mjs.map +1 -1
  50. package/es/packages/components/touchchat/utils/markdown.mjs +2 -2
  51. package/es/packages/components/touchchat/utils/markdown.mjs.map +1 -1
  52. package/es/packages/components/touchchat/utils/socket.mjs.map +1 -1
  53. package/es/packages/components/touchchat/utils/tools.mjs.map +1 -1
  54. package/es/packages/components/touchchat/utils/uuid.mjs.map +1 -1
  55. package/es/packages/utils/config.mjs.map +1 -1
  56. package/es/packages/utils/types.mjs.map +1 -1
  57. package/lib/index.js.map +1 -1
  58. package/lib/node_modules/.pnpm/{dompurify@3.4.3 → dompurify@3.4.11}/node_modules/dompurify/dist/purify.es.js +939 -226
  59. package/lib/node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.js.map +1 -0
  60. package/lib/node_modules/.pnpm/{linkify-it@5.0.0 → linkify-it@5.0.2}/node_modules/linkify-it/index.js +109 -105
  61. package/lib/node_modules/.pnpm/linkify-it@5.0.2/node_modules/linkify-it/index.js.map +1 -0
  62. package/lib/node_modules/.pnpm/{linkify-it@5.0.0 → linkify-it@5.0.2}/node_modules/linkify-it/lib/re.js +34 -30
  63. package/lib/node_modules/.pnpm/linkify-it@5.0.2/node_modules/linkify-it/lib/re.js.map +1 -0
  64. package/lib/node_modules/.pnpm/{markdown-it-deflist@3.0.0 → markdown-it-deflist@3.0.1}/node_modules/markdown-it-deflist/index.js +2 -1
  65. package/lib/node_modules/.pnpm/markdown-it-deflist@3.0.1/node_modules/markdown-it-deflist/index.js.map +1 -0
  66. package/lib/node_modules/.pnpm/markdown-it@14.1.0/node_modules/markdown-it/lib/index.js +1 -1
  67. package/lib/package.json.js +1 -1
  68. package/lib/packages/components/touchchat/component/AiRobot/face.vue2.js.map +1 -1
  69. package/lib/packages/components/touchchat/component/AiRobot/letter.vue2.js +1 -1
  70. package/lib/packages/components/touchchat/component/AiRobot/letter.vue2.js.map +1 -1
  71. package/lib/packages/components/touchchat/component/AiRobot/meterialPrepare.vue2.js.map +1 -1
  72. package/lib/packages/components/touchchat/component/AiRobot/start.vue2.js.map +1 -1
  73. package/lib/packages/components/touchchat/component/CardView.vue2.js.map +1 -1
  74. package/lib/packages/components/touchchat/component/ImageFile.vue2.js.map +1 -1
  75. package/lib/packages/components/touchchat/component/ImageView.vue2.js.map +1 -1
  76. package/lib/packages/components/touchchat/component/LinkView.vue2.js.map +1 -1
  77. package/lib/packages/components/touchchat/component/MarkLayer.vue2.js.map +1 -1
  78. package/lib/packages/components/touchchat/component/ModuleSelect.vue2.js.map +1 -1
  79. package/lib/packages/components/touchchat/component/Screenshot.vue2.js.map +1 -1
  80. package/lib/packages/components/touchchat/component/ThoughtChain.vue2.js.map +1 -1
  81. package/lib/packages/components/touchchat/component/UploadView.vue2.js.map +1 -1
  82. package/lib/packages/components/touchchat/component/UploadView_api.vue2.js.map +1 -1
  83. package/lib/packages/components/touchchat/index.js.map +1 -1
  84. package/lib/packages/components/touchchat/src/AiChat/Chat/TouchChat.js.map +1 -1
  85. package/lib/packages/components/touchchat/src/AiChat/Chat/useChat.js +6 -1
  86. package/lib/packages/components/touchchat/src/AiChat/Chat/useChat.js.map +1 -1
  87. package/lib/packages/components/touchchat/src/AiChat/Chat/useMessageRender.js +1 -1
  88. package/lib/packages/components/touchchat/src/AiChat/Chat/useSSE.js +10 -7
  89. package/lib/packages/components/touchchat/src/AiChat/Chat/useSSE.js.map +1 -1
  90. package/lib/packages/components/touchchat/src/AiChat/Chat/useScroll.js.map +1 -1
  91. package/lib/packages/components/touchchat/src/AiChat/ChatInput.vue2.js +5 -1
  92. package/lib/packages/components/touchchat/src/AiChat/ChatInput.vue2.js.map +1 -1
  93. package/lib/packages/components/touchchat/src/AiChat/HistoryList.vue2.js.map +1 -1
  94. package/lib/packages/components/touchchat/src/AiChat/HistorySidebar.vue2.js.map +1 -1
  95. package/lib/packages/components/touchchat/src/AiChat/SiderBarView.vue2.js.map +1 -1
  96. package/lib/packages/components/touchchat/src/AiChat/TouchAgent.vue2.js +136 -120
  97. package/lib/packages/components/touchchat/src/AiChat/TouchAgent.vue2.js.map +1 -1
  98. package/lib/packages/components/touchchat/src/index.vue2.js +6 -1
  99. package/lib/packages/components/touchchat/src/index.vue2.js.map +1 -1
  100. package/lib/packages/components/touchchat/src/types/a2a.js.map +1 -1
  101. package/lib/packages/components/touchchat/utils/a2aService.js +189 -132
  102. package/lib/packages/components/touchchat/utils/a2aService.js.map +1 -1
  103. package/lib/packages/components/touchchat/utils/chatRobot.js.map +1 -1
  104. package/lib/packages/components/touchchat/utils/deepCloneDom.js.map +1 -1
  105. package/lib/packages/components/touchchat/utils/hight-it.js.map +1 -1
  106. package/lib/packages/components/touchchat/utils/markdown.js +2 -2
  107. package/lib/packages/components/touchchat/utils/markdown.js.map +1 -1
  108. package/lib/packages/components/touchchat/utils/socket.js.map +1 -1
  109. package/lib/packages/components/touchchat/utils/tools.js.map +1 -1
  110. package/lib/packages/components/touchchat/utils/uuid.js.map +1 -1
  111. package/lib/packages/utils/config.js.map +1 -1
  112. package/lib/packages/utils/types.js.map +1 -1
  113. package/package.json +83 -83
  114. package/es/node_modules/.pnpm/dompurify@3.4.3/node_modules/dompurify/dist/purify.es.mjs.map +0 -1
  115. package/es/node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/index.mjs.map +0 -1
  116. package/es/node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/lib/re.mjs.map +0 -1
  117. package/es/node_modules/.pnpm/markdown-it-deflist@3.0.0/node_modules/markdown-it-deflist/index.mjs.map +0 -1
  118. package/lib/node_modules/.pnpm/dompurify@3.4.3/node_modules/dompurify/dist/purify.es.js.map +0 -1
  119. package/lib/node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/index.js.map +0 -1
  120. package/lib/node_modules/.pnpm/linkify-it@5.0.0/node_modules/linkify-it/lib/re.js.map +0 -1
  121. package/lib/node_modules/.pnpm/markdown-it-deflist@3.0.0/node_modules/markdown-it-deflist/index.js.map +0 -1
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.3/LICENSE */
1
+ /*! @license DOMPurify 3.4.11 | (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.11/LICENSE */
2
2
 
3
3
  function _arrayLikeToArray(r, a) {
4
4
  (null == a || a > r.length) && (a = r.length);
@@ -310,7 +310,7 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
310
310
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
311
311
  const text = freeze(['#text']);
312
312
 
313
- 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']);
313
+ 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']);
314
314
  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']);
315
315
  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']);
316
316
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
@@ -327,16 +327,31 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
327
327
  );
328
328
  const DOCTYPE_NAME = seal(/^html$/i);
329
329
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
330
+ // Markup-significant character probes used by _sanitizeElements.
331
+ // Shared module-level instances are safe despite the sticky /g flags:
332
+ // unapply() resets lastIndex for RegExp receivers before every call.
333
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
334
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
335
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
336
+ const SELF_CLOSING_TAG = seal(/\/>/i);
330
337
 
331
- /* eslint-disable @typescript-eslint/indent */
332
338
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
333
339
  const NODE_TYPE = {
334
340
  element: 1,
341
+ attribute: 2,
335
342
  text: 3,
343
+ cdataSection: 4,
344
+ entityReference: 5,
336
345
  // Deprecated
337
- progressingInstruction: 7,
346
+ entityNode: 6,
347
+ // Deprecated
348
+ processingInstruction: 7,
338
349
  comment: 8,
339
- document: 9};
350
+ document: 9,
351
+ documentType: 10,
352
+ documentFragment: 11,
353
+ notation: 12 // Deprecated
354
+ };
340
355
  const getGlobal = function getGlobal() {
341
356
  return typeof window === 'undefined' ? null : window;
342
357
  };
@@ -391,10 +406,25 @@ const _createHooksMap = function _createHooksMap() {
391
406
  uponSanitizeShadowNode: []
392
407
  };
393
408
  };
409
+ /**
410
+ * Resolve a set-valued configuration option: a fresh set built from
411
+ * cfg[key] when it is an own array property (seeded with a clone of
412
+ * options.base when given, case-normalized via options.transform),
413
+ * the fallback set otherwise.
414
+ *
415
+ * @param cfg the cloned, prototype-free configuration object
416
+ * @param key the configuration property to read
417
+ * @param fallback the set to use when the option is absent or not an array
418
+ * @param options transform and optional base set to merge into
419
+ * @returns the resolved set
420
+ */
421
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
422
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
423
+ };
394
424
  function createDOMPurify() {
395
425
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
396
426
  const DOMPurify = root => createDOMPurify(root);
397
- DOMPurify.version = '3.4.3';
427
+ DOMPurify.version = '3.4.11';
398
428
  DOMPurify.removed = [];
399
429
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
400
430
  // Not running in a browser, provide a factory function
@@ -405,15 +435,15 @@ function createDOMPurify() {
405
435
  let document = window.document;
406
436
  const originalDocument = document;
407
437
  const currentScript = originalDocument.currentScript;
408
- const DocumentFragment = window.DocumentFragment,
409
- HTMLTemplateElement = window.HTMLTemplateElement,
438
+ window.DocumentFragment;
439
+ const HTMLTemplateElement = window.HTMLTemplateElement,
410
440
  Node = window.Node,
411
441
  Element = window.Element,
412
442
  NodeFilter = window.NodeFilter,
413
- _window$NamedNodeMap = window.NamedNodeMap,
414
- NamedNodeMap = _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
415
- HTMLFormElement = window.HTMLFormElement,
416
- DOMParser = window.DOMParser,
443
+ _window$NamedNodeMap = window.NamedNodeMap;
444
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
445
+ window.HTMLFormElement;
446
+ const DOMParser = window.DOMParser,
417
447
  trustedTypes = window.trustedTypes;
418
448
  const ElementPrototype = Element.prototype;
419
449
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
@@ -421,6 +451,10 @@ function createDOMPurify() {
421
451
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
422
452
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
423
453
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
454
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
455
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
456
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
457
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
424
458
  // As per issue #47, the web-components registry is inherited by a
425
459
  // new document created via createHTMLDocument. As per the spec
426
460
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -435,6 +469,54 @@ function createDOMPurify() {
435
469
  }
436
470
  let trustedTypesPolicy;
437
471
  let emptyHTML = '';
472
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
473
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
474
+ // on duplicate policy names — and is the only policy allowed to persist
475
+ // across configurations and survive `clearConfig()`.
476
+ let defaultTrustedTypesPolicy;
477
+ let defaultTrustedTypesPolicyResolved = false;
478
+ // Tracks whether we are already inside a call to the configured Trusted Types
479
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
480
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
481
+ // re-enter the policy and recurse until the stack overflows. We detect that
482
+ // re-entry and throw a clear, actionable error instead. The guard is shared
483
+ // across both callbacks, because either one re-entering `sanitize` triggers
484
+ // the same unbounded recursion.
485
+ let IN_TRUSTED_TYPES_POLICY = 0;
486
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
487
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
488
+ 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.');
489
+ }
490
+ };
491
+ const _createTrustedHTML = function _createTrustedHTML(html) {
492
+ _assertNotInTrustedTypesPolicy();
493
+ IN_TRUSTED_TYPES_POLICY++;
494
+ try {
495
+ return trustedTypesPolicy.createHTML(html);
496
+ } finally {
497
+ IN_TRUSTED_TYPES_POLICY--;
498
+ }
499
+ };
500
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
501
+ _assertNotInTrustedTypesPolicy();
502
+ IN_TRUSTED_TYPES_POLICY++;
503
+ try {
504
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
505
+ } finally {
506
+ IN_TRUSTED_TYPES_POLICY--;
507
+ }
508
+ };
509
+ // Lazily resolve (and cache) the instance's internal default policy.
510
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
511
+ // repeated (Trusted Types throws on duplicate names), and a failed or
512
+ // unsupported attempt must not be retried on every parse.
513
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
514
+ if (!defaultTrustedTypesPolicyResolved) {
515
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
516
+ defaultTrustedTypesPolicyResolved = true;
517
+ }
518
+ return defaultTrustedTypesPolicy;
519
+ };
438
520
  const _document = document,
439
521
  implementation = _document.implementation,
440
522
  createNodeIterator = _document.createNodeIterator,
@@ -531,6 +613,13 @@ function createDOMPurify() {
531
613
  let WHOLE_DOCUMENT = false;
532
614
  /* Track whether config is already set on this instance of DOMPurify. */
533
615
  let SET_CONFIG = false;
616
+ /* Pristine allowlist bindings captured at setConfig() time. On the
617
+ * persistent-config path sanitize() restores the sets from these before
618
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
619
+ * carry across calls. Null until setConfig() is called; reset by
620
+ * clearConfig(). */
621
+ let SET_CONFIG_ALLOWED_TAGS = null;
622
+ let SET_CONFIG_ALLOWED_ATTR = null;
534
623
  /* Decide if all elements (e.g. style, script) must be children of
535
624
  * document.body. By default, browsers might move them to document.head */
536
625
  let FORCE_BODY = false;
@@ -573,7 +662,17 @@ function createDOMPurify() {
573
662
  let USE_PROFILES = {};
574
663
  /* Tags to ignore content of when KEEP_CONTENT is true */
575
664
  let FORBID_CONTENTS = null;
576
- 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']);
665
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
666
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
667
+ // the UA (customizable <select>) — including any on* handlers — and the
668
+ // engine re-mirrors synchronously whenever a removal changes which
669
+ // option/selectedcontent is current, even inside DOMPurify's inert
670
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
671
+ // mirror target ahead of the walk, which the engine refills, looping
672
+ // forever (DoS) and amplifying output. Dropping its content on removal
673
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
674
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
675
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
577
676
  /* Tags that are safe for data: URIs */
578
677
  let DATA_URI_TAGS = null;
579
678
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -589,8 +688,10 @@ function createDOMPurify() {
589
688
  /* Allowed XHTML+XML namespaces */
590
689
  let ALLOWED_NAMESPACES = null;
591
690
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
592
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
593
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
691
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
692
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
693
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
694
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
594
695
  // Certain elements are allowed in both SVG and HTML
595
696
  // namespace. We need to specify them explicitly
596
697
  // so that they don't get erroneously deleted from
@@ -632,14 +733,32 @@ function createDOMPurify() {
632
733
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
633
734
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
634
735
  /* Set configuration parameters */
635
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
636
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') && arrayIsArray(cfg.ALLOWED_ATTR) ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
637
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') && arrayIsArray(cfg.ALLOWED_NAMESPACES) ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
638
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR) ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
639
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') && arrayIsArray(cfg.ADD_DATA_URI_TAGS) ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
640
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS) ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
641
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') && arrayIsArray(cfg.FORBID_TAGS) ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
642
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR) ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
736
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
737
+ transform: transformCaseFunc
738
+ });
739
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
740
+ transform: transformCaseFunc
741
+ });
742
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
743
+ transform: stringToString
744
+ });
745
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
746
+ transform: transformCaseFunc,
747
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
748
+ });
749
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
750
+ transform: transformCaseFunc,
751
+ base: DEFAULT_DATA_URI_TAGS
752
+ });
753
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
754
+ transform: transformCaseFunc
755
+ });
756
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
757
+ transform: transformCaseFunc
758
+ });
759
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
760
+ transform: transformCaseFunc
761
+ });
643
762
  USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
644
763
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
645
764
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
@@ -658,8 +777,8 @@ function createDOMPurify() {
658
777
  IN_PLACE = cfg.IN_PLACE || false; // Default false
659
778
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
660
779
  NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
661
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); // Default built-in map
662
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, ['annotation-xml']); // Default built-in map
780
+ 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
781
+ 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
663
782
  const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
664
783
  CUSTOM_ELEMENT_HANDLING = create(null);
665
784
  if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
@@ -671,6 +790,7 @@ function createDOMPurify() {
671
790
  if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
672
791
  CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
673
792
  }
793
+ seal(CUSTOM_ELEMENT_HANDLING);
674
794
  if (SAFE_FOR_TEMPLATES) {
675
795
  ALLOW_DATA_ATTR = false;
676
796
  }
@@ -754,6 +874,13 @@ function createDOMPurify() {
754
874
  addToSet(ALLOWED_TAGS, ['tbody']);
755
875
  delete FORBID_TAGS.tbody;
756
876
  }
877
+ // Re-derive the active Trusted Types policy from this configuration on
878
+ // every parse. The active policy must never be sticky closure state that
879
+ // outlives the config that set it: a caller-supplied policy left in place
880
+ // after `clearConfig()` — or after a later call that supplied none, or
881
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
882
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
883
+ // See GHSA-vxr8-fq34-vvx9.
757
884
  if (cfg.TRUSTED_TYPES_POLICY) {
758
885
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
759
886
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -761,18 +888,45 @@ function createDOMPurify() {
761
888
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
762
889
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
763
890
  }
764
- // Overwrite existing TrustedTypes policy.
891
+ // A caller-supplied policy applies to this configuration only.
892
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
765
893
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
766
- // Sign local variables required by `sanitize`.
767
- emptyHTML = trustedTypesPolicy.createHTML('');
894
+ // Sign local variables required by `sanitize`. If the supplied policy's
895
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
896
+ // throws via the re-entrancy guard. Restore the previous policy first so
897
+ // the instance is not left in a poisoned state. See #1422.
898
+ try {
899
+ emptyHTML = _createTrustedHTML('');
900
+ } catch (error) {
901
+ trustedTypesPolicy = previousTrustedTypesPolicy;
902
+ throw error;
903
+ }
904
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
905
+ // Explicit opt-out for this call: perform no Trusted Types signing and
906
+ // create nothing (so a strict `trusted-types` CSP that disallows a
907
+ // `dompurify` policy can still call `sanitize` from inside its own
908
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
909
+ // `null` also drops any previously retained caller policy, so it cannot
910
+ // resurface on a later call, while still allowing the next config-less
911
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
912
+ trustedTypesPolicy = undefined;
913
+ emptyHTML = '';
768
914
  } else {
769
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
915
+ // No policy supplied: keep the currently active policy if one is set — a
916
+ // previously supplied policy is intentionally sticky across config-less
917
+ // calls — otherwise fall back to the instance's own internal policy,
918
+ // created at most once. (A policy supplied for a *single* call still
919
+ // lingers by design; what must not linger is a policy whose configuration
920
+ // has been torn down via `clearConfig()`, which restores the default.)
770
921
  if (trustedTypesPolicy === undefined) {
771
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
922
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
772
923
  }
773
- // If creating the internal policy succeeded sign internal variables.
774
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
775
- emptyHTML = trustedTypesPolicy.createHTML('');
924
+ // Sign internal variables only when a policy is active. A falsy policy
925
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
926
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
927
+ // a non-policy and throw. See #1422.
928
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
929
+ emptyHTML = _createTrustedHTML('');
776
930
  }
777
931
  }
778
932
  // Prevent further manipulation of configuration.
@@ -787,6 +941,77 @@ function createDOMPurify() {
787
941
  * correctly. */
788
942
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
789
943
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
944
+ /**
945
+ * Namespace rules for an element in the SVG namespace.
946
+ *
947
+ * @param tagName the element's lowercase tag name
948
+ * @param parent the (possibly simulated) parent node
949
+ * @param parentTagName the parent's lowercase tag name
950
+ * @returns true if a spec-compliant parser could produce this element
951
+ */
952
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
953
+ // The only way to switch from HTML namespace to SVG
954
+ // is via <svg>. If it happens via any other tag, then
955
+ // it should be killed.
956
+ if (parent.namespaceURI === HTML_NAMESPACE) {
957
+ return tagName === 'svg';
958
+ }
959
+ // The only way to switch from MathML to SVG is via <svg>
960
+ // if the parent is either <annotation-xml> or a MathML
961
+ // text integration point.
962
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
963
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
964
+ }
965
+ // We only allow elements that are defined in SVG
966
+ // spec. All others are disallowed in SVG namespace.
967
+ return Boolean(ALL_SVG_TAGS[tagName]);
968
+ };
969
+ /**
970
+ * Namespace rules for an element in the MathML namespace.
971
+ *
972
+ * @param tagName the element's lowercase tag name
973
+ * @param parent the (possibly simulated) parent node
974
+ * @param parentTagName the parent's lowercase tag name
975
+ * @returns true if a spec-compliant parser could produce this element
976
+ */
977
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
978
+ // The only way to switch from HTML namespace to MathML
979
+ // is via <math>. If it happens via any other tag, then
980
+ // it should be killed.
981
+ if (parent.namespaceURI === HTML_NAMESPACE) {
982
+ return tagName === 'math';
983
+ }
984
+ // The only way to switch from SVG to MathML is via
985
+ // <math> and HTML integration points
986
+ if (parent.namespaceURI === SVG_NAMESPACE) {
987
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
988
+ }
989
+ // We only allow elements that are defined in MathML
990
+ // spec. All others are disallowed in MathML namespace.
991
+ return Boolean(ALL_MATHML_TAGS[tagName]);
992
+ };
993
+ /**
994
+ * Namespace rules for an element in the HTML namespace.
995
+ *
996
+ * @param tagName the element's lowercase tag name
997
+ * @param parent the (possibly simulated) parent node
998
+ * @param parentTagName the parent's lowercase tag name
999
+ * @returns true if a spec-compliant parser could produce this element
1000
+ */
1001
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
1002
+ // The only way to switch from SVG to HTML is via
1003
+ // HTML integration points, and from MathML to HTML
1004
+ // is via MathML text integration points
1005
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
1006
+ return false;
1007
+ }
1008
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
1009
+ return false;
1010
+ }
1011
+ // We disallow tags that are specific for MathML
1012
+ // or SVG and should never appear in HTML namespace
1013
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1014
+ };
790
1015
  /**
791
1016
  * @param element a DOM element whose namespace is being checked
792
1017
  * @returns Return false if the element has a
@@ -809,51 +1034,13 @@ function createDOMPurify() {
809
1034
  return false;
810
1035
  }
811
1036
  if (element.namespaceURI === SVG_NAMESPACE) {
812
- // The only way to switch from HTML namespace to SVG
813
- // is via <svg>. If it happens via any other tag, then
814
- // it should be killed.
815
- if (parent.namespaceURI === HTML_NAMESPACE) {
816
- return tagName === 'svg';
817
- }
818
- // The only way to switch from MathML to SVG is via`
819
- // svg if parent is either <annotation-xml> or MathML
820
- // text integration points.
821
- if (parent.namespaceURI === MATHML_NAMESPACE) {
822
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
823
- }
824
- // We only allow elements that are defined in SVG
825
- // spec. All others are disallowed in SVG namespace.
826
- return Boolean(ALL_SVG_TAGS[tagName]);
1037
+ return _checkSvgNamespace(tagName, parent, parentTagName);
827
1038
  }
828
1039
  if (element.namespaceURI === MATHML_NAMESPACE) {
829
- // The only way to switch from HTML namespace to MathML
830
- // is via <math>. If it happens via any other tag, then
831
- // it should be killed.
832
- if (parent.namespaceURI === HTML_NAMESPACE) {
833
- return tagName === 'math';
834
- }
835
- // The only way to switch from SVG to MathML is via
836
- // <math> and HTML integration points
837
- if (parent.namespaceURI === SVG_NAMESPACE) {
838
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
839
- }
840
- // We only allow elements that are defined in MathML
841
- // spec. All others are disallowed in MathML namespace.
842
- return Boolean(ALL_MATHML_TAGS[tagName]);
1040
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
843
1041
  }
844
1042
  if (element.namespaceURI === HTML_NAMESPACE) {
845
- // The only way to switch from SVG to HTML is via
846
- // HTML integration points, and from MathML to HTML
847
- // is via MathML text integration points
848
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
849
- return false;
850
- }
851
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
852
- return false;
853
- }
854
- // We disallow tags that are specific for MathML
855
- // or SVG and should never appear in HTML namespace
856
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1043
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
857
1044
  }
858
1045
  // For XHTML and XML documents that support custom namespaces
859
1046
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -878,7 +1065,74 @@ function createDOMPurify() {
878
1065
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
879
1066
  getParentNode(node).removeChild(node);
880
1067
  } catch (_) {
1068
+ /* The normal detach failed — this is reached for a parentless node
1069
+ (getParentNode() is null, so .removeChild throws). Element.prototype
1070
+ .remove() is itself a spec no-op on a parentless node, so a recorded
1071
+ "removal" would otherwise hand the caller back an intact,
1072
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
1073
+ the style-with-element-child rule decided to kill). Fail closed by
1074
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
1075
+ rather than trying to "neutralize" the node via its own methods.
1076
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
1077
+ on the node, both of which a <form> root can clobber via a named child
1078
+ (and _isClobbered does not even probe getAttributeNames), so the
1079
+ neutralize step could itself be silently defeated, leaving the payload
1080
+ intact. A throw touches only the cached, clobber-safe remove() and
1081
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
1082
+ to every root-kill reason. REPORT-3.
1083
+ This lives inside the catch, so it never fires for a normally-removed
1084
+ in-tree node: those have a parent, removeChild() succeeds, and the
1085
+ catch is not entered. Only a kept (parentless) root reaches here. */
881
1086
  remove(node);
1087
+ if (!getParentNode(node)) {
1088
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
1089
+ }
1090
+ }
1091
+ };
1092
+ /**
1093
+ * _neutralizeRoot
1094
+ *
1095
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
1096
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1097
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1098
+ * parentless guard throws, or any other re-entrant engine mutation — would
1099
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
1100
+ * after the abort point still carrying its handlers. There is no safe way
1101
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
1102
+ * remove every child and every attribute, then let the caller's catch see
1103
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1104
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1105
+ *
1106
+ * @param root the in-place root to empty
1107
+ */
1108
+ const _neutralizeRoot = function _neutralizeRoot(root) {
1109
+ const childNodes = getChildNodes(root);
1110
+ if (childNodes) {
1111
+ const snapshot = [];
1112
+ arrayForEach(childNodes, child => {
1113
+ arrayPush(snapshot, child);
1114
+ });
1115
+ arrayForEach(snapshot, child => {
1116
+ try {
1117
+ remove(child);
1118
+ } catch (_) {
1119
+ /* Best-effort teardown; a still-attached child is handled below */
1120
+ }
1121
+ });
1122
+ }
1123
+ const attributes = getAttributes(root);
1124
+ if (attributes) {
1125
+ for (let i = attributes.length - 1; i >= 0; --i) {
1126
+ const attribute = attributes[i];
1127
+ const name = attribute && attribute.name;
1128
+ if (typeof name === 'string') {
1129
+ try {
1130
+ root.removeAttribute(name);
1131
+ } catch (_) {
1132
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
1133
+ }
1134
+ }
1135
+ }
882
1136
  }
883
1137
  };
884
1138
  /**
@@ -913,6 +1167,72 @@ function createDOMPurify() {
913
1167
  }
914
1168
  }
915
1169
  };
1170
+ /**
1171
+ * _stripDisallowedAttributes
1172
+ *
1173
+ * Removes every attribute the active configuration does not allow from a
1174
+ * single element, using the same allowlist as the main attribute pass (so
1175
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1176
+ * neutralise nodes that are being discarded from an in-place tree.
1177
+ *
1178
+ * @param element the element to strip
1179
+ */
1180
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
1181
+ const attributes = getAttributes(element);
1182
+ if (!attributes) {
1183
+ return;
1184
+ }
1185
+ for (let i = attributes.length - 1; i >= 0; --i) {
1186
+ const attribute = attributes[i];
1187
+ const name = attribute && attribute.name;
1188
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
1189
+ continue;
1190
+ }
1191
+ try {
1192
+ element.removeAttribute(name);
1193
+ } catch (_) {
1194
+ /* Clobbered removeAttribute on a doomed node — ignore */
1195
+ }
1196
+ }
1197
+ };
1198
+ /**
1199
+ * _neutralizeSubtree
1200
+ *
1201
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1202
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1203
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1204
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1205
+ * those dropped nodes are detached from the caller's LIVE tree but a
1206
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1207
+ * loading) keeps its queued resource event, which fires in page scope after
1208
+ * sanitize returns. This walks a removed subtree and strips every attribute
1209
+ * the active configuration does not allow — so `on*` handlers are cancelled
1210
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1211
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
1212
+ * queued event can fire. Hook-free by design: these nodes leave the output,
1213
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1214
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1215
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1216
+ * the `<img>`, are reached and scrubbed).
1217
+ *
1218
+ * @param root the root of a removed subtree to neutralise
1219
+ */
1220
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1221
+ const stack = [root];
1222
+ while (stack.length > 0) {
1223
+ const node = stack.pop();
1224
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1225
+ if (nodeType === NODE_TYPE.element) {
1226
+ _stripDisallowedAttributes(node);
1227
+ }
1228
+ const childNodes = getChildNodes(node);
1229
+ if (childNodes) {
1230
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1231
+ stack.push(childNodes[i]);
1232
+ }
1233
+ }
1234
+ }
1235
+ };
916
1236
  /**
917
1237
  * _initDocument
918
1238
  *
@@ -934,7 +1254,7 @@ function createDOMPurify() {
934
1254
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
935
1255
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
936
1256
  }
937
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
1257
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
938
1258
  /*
939
1259
  * Use the DOMParser API by default, fallback later if needs be
940
1260
  * DOMParser not work for svg when has multiple root element.
@@ -974,29 +1294,254 @@ function createDOMPurify() {
974
1294
  // eslint-disable-next-line no-bitwise
975
1295
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
976
1296
  };
1297
+ /**
1298
+ * Replace template expression syntax (mustache, ERB, template
1299
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1300
+ * sites. Order matters: mustache, then ERB, then template literal.
1301
+ *
1302
+ * @param value the string to scrub
1303
+ * @returns the scrubbed string
1304
+ */
1305
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
1306
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
1307
+ value = stringReplace(value, ERB_EXPR$1, ' ');
1308
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
1309
+ return value;
1310
+ };
1311
+ /**
1312
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1313
+ * character data of an element subtree. Used as the final safety net for
1314
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
1315
+ * which only form after text-node normalization (e.g. fragments split across
1316
+ * stripped elements) cannot survive into a template-evaluating framework.
1317
+ *
1318
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
1319
+ * in place rather than round-tripping through innerHTML. This preserves
1320
+ * descendant node references (important for IN_PLACE callers), avoids a
1321
+ * serialize/reparse cycle, and reads literal character data — which means
1322
+ * `<%...%>` in text content matches the ERB regex against its real bytes
1323
+ * instead of the HTML-entity-escaped form innerHTML would produce.
1324
+ *
1325
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
1326
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
1327
+ *
1328
+ * @param node The root element whose character data should be scrubbed.
1329
+ */
1330
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1331
+ var _node$querySelectorAl;
1332
+ node.normalize();
1333
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
1334
+ // eslint-disable-next-line no-bitwise
1335
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1336
+ let currentNode = walker.nextNode();
1337
+ while (currentNode) {
1338
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1339
+ currentNode = walker.nextNode();
1340
+ }
1341
+ // NodeIterator does not descend into <template>.content per the DOM spec,
1342
+ // so we must explicitly recurse into each template's content fragment,
1343
+ // mirroring the approach used by _sanitizeShadowDOM.
1344
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
1345
+ if (templates) {
1346
+ arrayForEach(templates, tmpl => {
1347
+ if (_isDocumentFragment(tmpl.content)) {
1348
+ _scrubTemplateExpressions2(tmpl.content);
1349
+ }
1350
+ });
1351
+ }
1352
+ };
977
1353
  /**
978
1354
  * _isClobbered
979
1355
  *
1356
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
1357
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
1358
+ * `name` attribute matching a prototype property shadows that property
1359
+ * on direct reads. We use this check at the IN_PLACE entry-point and
1360
+ * during attribute sanitization to refuse clobbered forms.
1361
+ *
980
1362
  * @param element element to check for clobbering attacks
981
1363
  * @return true if clobbered, false if safe
982
1364
  */
983
1365
  const _isClobbered = function _isClobbered(element) {
984
- 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');
1366
+ // Realm-independent tag-name probe. If we can't determine the tag
1367
+ // name at all, we can't reason about clobbering — return false
1368
+ // (the caller's other defences still apply).
1369
+ const realTagName = getNodeName ? getNodeName(element) : null;
1370
+ if (typeof realTagName !== 'string') {
1371
+ return false;
1372
+ }
1373
+ if (transformCaseFunc(realTagName) !== 'form') {
1374
+ return false;
1375
+ }
1376
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
1377
+ // Realm-safe NamedNodeMap detection: equality against the cached
1378
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
1379
+ // makes the direct read diverge from the cached read; a clean form
1380
+ // (same-realm OR foreign-realm) has both reads pointing at the same
1381
+ // canonical NamedNodeMap.
1382
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
1383
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
1384
+ // returns the integer 1 for any Element regardless of realm; direct
1385
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
1386
+ // the named child element. Cheap addition — nodeType is read from
1387
+ // an internal slot, no serialization cost — and removes a residual
1388
+ // clobbering surface used by several mXSS / PI / comment branches
1389
+ // in _sanitizeElements that compare currentNode.nodeType directly.
1390
+ element.nodeType !== getNodeType(element) ||
1391
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
1392
+ // "childNodes" shadows the prototype getter. Direct reads of
1393
+ // form.childNodes from a clobbered form return the named child
1394
+ // instead of the real NodeList, so any walk that reads it directly
1395
+ // skips the form's real children. Compare the direct read to the
1396
+ // cached Node.prototype getter — when the form's named-property
1397
+ // getter intercepts the read, the two values differ and we flag
1398
+ // the form. This catches every clobbering child type (input,
1399
+ // select, etc.) regardless of whether the named child happens to
1400
+ // carry a numeric .length, which a typeof-based probe would miss
1401
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
1402
+ element.childNodes !== getChildNodes(element);
1403
+ };
1404
+ /**
1405
+ * Checks whether the given value is a DocumentFragment from any realm.
1406
+ *
1407
+ * The realm-independent replacement reads `nodeType` through the cached
1408
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
1409
+ * constant (11). nodeType is a numeric value resolved from the node's
1410
+ * internal slot, identical across realms for the same kind of node.
1411
+ *
1412
+ * @param value object to check
1413
+ * @return true if value is a DocumentFragment-shaped node from any realm
1414
+ */
1415
+ const _isDocumentFragment = function _isDocumentFragment(value) {
1416
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1417
+ return false;
1418
+ }
1419
+ try {
1420
+ return getNodeType(value) === NODE_TYPE.documentFragment;
1421
+ } catch (_) {
1422
+ return false;
1423
+ }
985
1424
  };
986
1425
  /**
987
- * Checks whether the given object is a DOM node.
1426
+ * Checks whether the given object is a DOM node, including nodes that
1427
+ * originate from a different window/realm (e.g. an iframe's
1428
+ * contentDocument). The previous `value instanceof Node` check was
1429
+ * realm-bound: nodes from a different window failed it, causing
1430
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
1431
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
988
1432
  *
989
1433
  * @param value object to check whether it's a DOM node
990
- * @return true is object is a DOM node
1434
+ * @return true if value is a DOM node from any realm
991
1435
  */
992
1436
  const _isNode = function _isNode(value) {
993
- return typeof Node === 'function' && value instanceof Node;
1437
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1438
+ return false;
1439
+ }
1440
+ try {
1441
+ return typeof getNodeType(value) === 'number';
1442
+ } catch (_) {
1443
+ return false;
1444
+ }
994
1445
  };
995
1446
  function _executeHooks(hooks, currentNode, data) {
1447
+ if (hooks.length === 0) {
1448
+ return;
1449
+ }
996
1450
  arrayForEach(hooks, hook => {
997
1451
  hook.call(DOMPurify, currentNode, data, CONFIG);
998
1452
  });
999
1453
  }
1454
+ /**
1455
+ * Structural-threat checks that condemn a node regardless of the
1456
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1457
+ * processing instructions, markup-bearing comments. Pure predicate;
1458
+ * the caller removes. Check order is load-bearing.
1459
+ *
1460
+ * @param currentNode the node to inspect
1461
+ * @param tagName the node's transformCaseFunc'd tag name
1462
+ * @return true if the node must be removed
1463
+ */
1464
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1465
+ /* Detect mXSS attempts abusing namespace confusion */
1466
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
1467
+ return true;
1468
+ }
1469
+ /* Remove risky CSS construction leading to mXSS */
1470
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1471
+ return true;
1472
+ }
1473
+ /* Remove any occurrence of processing instructions */
1474
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1475
+ return true;
1476
+ }
1477
+ /* Remove any kind of possibly harmful comments */
1478
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
1479
+ return true;
1480
+ }
1481
+ return false;
1482
+ };
1483
+ /**
1484
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1485
+ * allowed custom elements (false return exits _sanitizeElements
1486
+ * early - namespace/fallback checks and the afterSanitizeElements
1487
+ * hook are intentionally skipped for kept custom elements), else
1488
+ * hoist content per KEEP_CONTENT and remove.
1489
+ *
1490
+ * @param currentNode the disallowed node
1491
+ * @param tagName the node's transformCaseFunc'd tag name
1492
+ * @return true if the node was removed, false if kept
1493
+ */
1494
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1495
+ /* Check if we have a custom element to handle */
1496
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1497
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1498
+ return false;
1499
+ }
1500
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1501
+ return false;
1502
+ }
1503
+ }
1504
+ /* Keep content except for bad-listed elements.
1505
+ Use the cached prototype getters exclusively — the previous code
1506
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1507
+ fallbacks, but the cached getters always return the canonical
1508
+ value (or null for a real parent-less node), so the fallback
1509
+ path was dead in safe cases and a clobbering surface in unsafe
1510
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
1511
+ parentNode)` check already gates correctly. */
1512
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1513
+ const parentNode = getParentNode(currentNode);
1514
+ const childNodes = getChildNodes(currentNode);
1515
+ if (childNodes && parentNode) {
1516
+ const childCount = childNodes.length;
1517
+ /* In-place: hoist the *original* children so the iterator visits
1518
+ and sanitises them through the same allowlist pass as every other
1519
+ node. The caller built the tree in the live document, so the
1520
+ originals carry already-queued resource events (`<img onerror>`,
1521
+ `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
1522
+ those originals detached but still armed, firing in page scope
1523
+ while the returned tree looked clean. Moving is safe in-place: the
1524
+ root is pre-validated as an allowed tag and so is never the node
1525
+ being removed, which keeps `parentNode` inside the iterator root
1526
+ and the relocated child inside the serialised tree.
1527
+ Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1528
+ at — and the result serialised from — `body`, so a restrictive
1529
+ ALLOWED_TAGS that removes `body` itself must leave its content in
1530
+ place, which only cloning does; and those paths parse into an
1531
+ inert document, so their discarded originals never had a queued
1532
+ event to neutralise.
1533
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1534
+ valid whether we move (drops the trailing entry) or clone (leaves
1535
+ the list intact). */
1536
+ for (let i = childCount - 1; i >= 0; --i) {
1537
+ const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1538
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1539
+ }
1540
+ }
1541
+ }
1542
+ _forceRemove(currentNode);
1543
+ return true;
1544
+ };
1000
1545
  /**
1001
1546
  * _sanitizeElements
1002
1547
  *
@@ -1007,7 +1552,6 @@ function createDOMPurify() {
1007
1552
  * @return true if node was killed, false if left alive
1008
1553
  */
1009
1554
  const _sanitizeElements = function _sanitizeElements(currentNode) {
1010
- let content = null;
1011
1555
  /* Execute a hook if present */
1012
1556
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1013
1557
  /* Check if element is clobbered or can clobber */
@@ -1016,75 +1560,41 @@ function createDOMPurify() {
1016
1560
  return true;
1017
1561
  }
1018
1562
  /* Now let's check the element's type and name */
1019
- const tagName = transformCaseFunc(currentNode.nodeName);
1563
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1020
1564
  /* Execute a hook if present */
1021
1565
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1022
1566
  tagName,
1023
1567
  allowedTags: ALLOWED_TAGS
1024
1568
  });
1025
- /* Detect mXSS attempts abusing namespace confusion */
1026
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
1027
- _forceRemove(currentNode);
1028
- return true;
1029
- }
1030
- /* Remove risky CSS construction leading to mXSS */
1031
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1032
- _forceRemove(currentNode);
1033
- return true;
1034
- }
1035
- /* Remove any occurrence of processing instructions */
1036
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
1037
- _forceRemove(currentNode);
1038
- return true;
1039
- }
1040
- /* Remove any kind of possibly harmful comments */
1041
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
1569
+ /* Remove mXSS vectors, processing instructions and risky comments */
1570
+ if (_isUnsafeNode(currentNode, tagName)) {
1042
1571
  _forceRemove(currentNode);
1043
1572
  return true;
1044
1573
  }
1045
1574
  /* Remove element if anything forbids its presence */
1046
1575
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1047
- /* Check if we have a custom element to handle */
1048
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1049
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1050
- return false;
1051
- }
1052
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1053
- return false;
1054
- }
1055
- }
1056
- /* Keep content except for bad-listed elements */
1057
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1058
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
1059
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
1060
- if (childNodes && parentNode) {
1061
- const childCount = childNodes.length;
1062
- for (let i = childCount - 1; i >= 0; --i) {
1063
- const childClone = cloneNode(childNodes[i], true);
1064
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
1065
- }
1066
- }
1067
- }
1068
- _forceRemove(currentNode);
1069
- return true;
1070
- }
1071
- /* Check whether element has a valid namespace */
1072
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
1576
+ return _sanitizeDisallowedNode(currentNode, tagName);
1577
+ }
1578
+ /* Check whether element has a valid namespace.
1579
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1580
+ nodeType getter rather than `instanceof Element`, which is realm-
1581
+ bound and short-circuits to false for any node minted in a different
1582
+ realm — letting a foreign-realm element with a forbidden namespace
1583
+ slip past the namespace check entirely. */
1584
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
1585
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
1073
1586
  _forceRemove(currentNode);
1074
1587
  return true;
1075
1588
  }
1076
1589
  /* Make sure that older browsers don't get fallback-tag mXSS */
1077
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
1590
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
1078
1591
  _forceRemove(currentNode);
1079
1592
  return true;
1080
1593
  }
1081
1594
  /* Sanitize element content to be template-safe */
1082
1595
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1083
1596
  /* Get the element's text content */
1084
- content = currentNode.textContent;
1085
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1086
- content = stringReplace(content, expr, ' ');
1087
- });
1597
+ const content = _stripTemplateExpressions(currentNode.textContent);
1088
1598
  if (currentNode.textContent !== content) {
1089
1599
  arrayPush(DOMPurify.removed, {
1090
1600
  element: currentNode.cloneNode()
@@ -1119,7 +1629,7 @@ function createDOMPurify() {
1119
1629
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1120
1630
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1121
1631
  We don't need to check the value; it's always URI safe. */
1122
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1632
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
1123
1633
  if (
1124
1634
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1125
1635
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1151,6 +1661,63 @@ function createDOMPurify() {
1151
1661
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1152
1662
  return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1153
1663
  };
1664
+ /**
1665
+ * Wrap an attribute value in the matching Trusted Types object when
1666
+ * the active policy requires it. Namespaced attributes pass through
1667
+ * unchanged (no TT support yet, see
1668
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1669
+ *
1670
+ * @param lcTag lowercase tag name of the containing element
1671
+ * @param lcName lowercase attribute name
1672
+ * @param namespaceURI the attribute's namespace, if any
1673
+ * @param value the attribute value to wrap
1674
+ * @return the value, wrapped when Trusted Types demand it
1675
+ */
1676
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
1677
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
1678
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1679
+ case 'TrustedHTML':
1680
+ {
1681
+ return _createTrustedHTML(value);
1682
+ }
1683
+ case 'TrustedScriptURL':
1684
+ {
1685
+ return _createTrustedScriptURL(value);
1686
+ }
1687
+ }
1688
+ }
1689
+ return value;
1690
+ };
1691
+ /**
1692
+ * Write a modified attribute value back onto the element. On
1693
+ * success, re-probe for clobbering introduced by the new value and
1694
+ * remove the element when found; otherwise pop the removal entry
1695
+ * recorded by the earlier _removeAttribute (long-standing pairing
1696
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
1697
+ * failure, remove the attribute instead.
1698
+ *
1699
+ * @param currentNode the element carrying the attribute
1700
+ * @param name the attribute name as present on the element
1701
+ * @param namespaceURI the attribute's namespace, if any
1702
+ * @param value the new attribute value
1703
+ */
1704
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
1705
+ try {
1706
+ if (namespaceURI) {
1707
+ currentNode.setAttributeNS(namespaceURI, name, value);
1708
+ } else {
1709
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1710
+ currentNode.setAttribute(name, value);
1711
+ }
1712
+ if (_isClobbered(currentNode)) {
1713
+ _forceRemove(currentNode);
1714
+ } else {
1715
+ arrayPop(DOMPurify.removed);
1716
+ }
1717
+ } catch (_) {
1718
+ _removeAttribute(name, currentNode);
1719
+ }
1720
+ };
1154
1721
  /**
1155
1722
  * _sanitizeAttributes
1156
1723
  *
@@ -1177,6 +1744,7 @@ function createDOMPurify() {
1177
1744
  forceKeepAttr: undefined
1178
1745
  };
1179
1746
  let l = attributes.length;
1747
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1180
1748
  /* Go backwards over all attributes; safely remove bad ones */
1181
1749
  while (l--) {
1182
1750
  const attr = attributes[l];
@@ -1214,7 +1782,7 @@ function createDOMPurify() {
1214
1782
  _removeAttribute(name, currentNode);
1215
1783
  continue;
1216
1784
  }
1217
- /* Did the hooks approve of the attribute? */
1785
+ /* Did the hooks force-keep the attribute? */
1218
1786
  if (hookEvent.forceKeepAttr) {
1219
1787
  continue;
1220
1788
  }
@@ -1224,56 +1792,24 @@ function createDOMPurify() {
1224
1792
  continue;
1225
1793
  }
1226
1794
  /* Work around a security issue in jQuery 3.0 */
1227
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
1795
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
1228
1796
  _removeAttribute(name, currentNode);
1229
1797
  continue;
1230
1798
  }
1231
1799
  /* Sanitize attribute content to be template-safe */
1232
1800
  if (SAFE_FOR_TEMPLATES) {
1233
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1234
- value = stringReplace(value, expr, ' ');
1235
- });
1801
+ value = _stripTemplateExpressions(value);
1236
1802
  }
1237
1803
  /* Is `value` valid for this attribute? */
1238
- const lcTag = transformCaseFunc(currentNode.nodeName);
1239
1804
  if (!_isValidAttribute(lcTag, lcName, value)) {
1240
1805
  _removeAttribute(name, currentNode);
1241
1806
  continue;
1242
1807
  }
1243
1808
  /* Handle attributes that require Trusted Types */
1244
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
1245
- if (namespaceURI) ; else {
1246
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1247
- case 'TrustedHTML':
1248
- {
1249
- value = trustedTypesPolicy.createHTML(value);
1250
- break;
1251
- }
1252
- case 'TrustedScriptURL':
1253
- {
1254
- value = trustedTypesPolicy.createScriptURL(value);
1255
- break;
1256
- }
1257
- }
1258
- }
1259
- }
1809
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1260
1810
  /* Handle invalid data-* attribute set by try-catching it */
1261
1811
  if (value !== initValue) {
1262
- try {
1263
- if (namespaceURI) {
1264
- currentNode.setAttributeNS(namespaceURI, name, value);
1265
- } else {
1266
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1267
- currentNode.setAttribute(name, value);
1268
- }
1269
- if (_isClobbered(currentNode)) {
1270
- _forceRemove(currentNode);
1271
- } else {
1272
- arrayPop(DOMPurify.removed);
1273
- }
1274
- } catch (_) {
1275
- _removeAttribute(name, currentNode);
1276
- }
1812
+ _setAttributeValue(currentNode, name, namespaceURI, value);
1277
1813
  }
1278
1814
  }
1279
1815
  /* Execute a hook if present */
@@ -1296,10 +1832,31 @@ function createDOMPurify() {
1296
1832
  _sanitizeElements(shadowNode);
1297
1833
  /* Check attributes next */
1298
1834
  _sanitizeAttributes(shadowNode);
1299
- /* Deep shadow DOM detected */
1300
- if (shadowNode.content instanceof DocumentFragment) {
1835
+ /* Deep shadow DOM detected.
1836
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
1837
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
1838
+ recurse into <template>.content from foreign realms too. */
1839
+ if (_isDocumentFragment(shadowNode.content)) {
1301
1840
  _sanitizeShadowDOM2(shadowNode.content);
1302
1841
  }
1842
+ /* An element iterated here may itself host an attached
1843
+ shadow root. The default NodeIterator does not enter shadow
1844
+ trees, so a shadow root nested inside template.content was
1845
+ previously reached by no walk at all (the pre-pass at
1846
+ _sanitizeAttachedShadowRoots descends via childNodes, which
1847
+ doesn't enter template.content; the template-content recursion
1848
+ above iterates the content but never inspected shadowRoot).
1849
+ Walk it explicitly. The nodeType guard avoids reading
1850
+ shadowRoot off text / comment / CDATA / PI nodes that the
1851
+ iterator also surfaces. */
1852
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
1853
+ if (shadowNodeType === NODE_TYPE.element) {
1854
+ const innerSr = getShadowRoot(shadowNode);
1855
+ if (_isDocumentFragment(innerSr)) {
1856
+ _sanitizeAttachedShadowRoots(innerSr);
1857
+ _sanitizeShadowDOM2(innerSr);
1858
+ }
1859
+ }
1303
1860
  }
1304
1861
  /* Execute a hook if present */
1305
1862
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
@@ -1323,28 +1880,83 @@ function createDOMPurify() {
1323
1880
  *
1324
1881
  * @param root the subtree root to walk for attached shadow roots
1325
1882
  */
1326
- const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1327
- if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
1328
- const sr = root.shadowRoot;
1329
- // Recurse first so that nested shadow roots are reached even if
1330
- // _sanitizeShadowDOM removes hosts at this level.
1331
- _sanitizeAttachedShadowRoots2(sr);
1332
- _sanitizeShadowDOM2(sr);
1333
- }
1334
- // Snapshot children before recursing. Sanitization of one subtree
1335
- // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1336
- // and naive nextSibling traversal would silently skip the rest of
1337
- // the list once a node is detached.
1338
- const childNodes = root.childNodes;
1339
- if (!childNodes) {
1340
- return;
1341
- }
1342
- const snapshot = [];
1343
- arrayForEach(childNodes, child => {
1344
- arrayPush(snapshot, child);
1345
- });
1346
- for (const child of snapshot) {
1347
- _sanitizeAttachedShadowRoots2(child);
1883
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
1884
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
1885
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
1886
+ built straight into the DOM the IN_PLACE surface) deeper than the JS
1887
+ call-stack budget would otherwise overflow native recursion here and
1888
+ throw at the IN_PLACE entry pre-pass, before a single node is
1889
+ sanitized, leaving the caller's live tree untouched (fail-open). See
1890
+ campaign-3 F4. A heap stack keeps depth off the call stack.
1891
+ Each work item is either a node to descend into, or a deferred
1892
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
1893
+ form preserves the original post-order discipline: a shadow root's
1894
+ nested shadow roots are discovered before the outer shadow is
1895
+ sanitized (which may remove hosts). Pushes are in reverse of the
1896
+ desired processing order (LIFO): template content, then children, then
1897
+ the shadow-sanitize, then the shadow walk — so the order matches the
1898
+ previous recursion exactly. */
1899
+ const stack = [{
1900
+ node: root,
1901
+ shadow: null
1902
+ }];
1903
+ while (stack.length > 0) {
1904
+ const item = stack.pop();
1905
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
1906
+ if (item.shadow) {
1907
+ _sanitizeShadowDOM2(item.shadow);
1908
+ continue;
1909
+ }
1910
+ const node = item.node;
1911
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1912
+ const isElement = nodeType === NODE_TYPE.element;
1913
+ /* (pushed last → processed first) Children, snapshotted in reverse so
1914
+ the first child is processed first. Snapshotting matters because a
1915
+ hook may detach siblings mid-walk. */
1916
+ const childNodes = getChildNodes(node);
1917
+ if (childNodes) {
1918
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1919
+ stack.push({
1920
+ node: childNodes[i],
1921
+ shadow: null
1922
+ });
1923
+ }
1924
+ }
1925
+ /* (pushed before children → processed after them, matching the old
1926
+ "template content last" order) When the node is a <template>,
1927
+ descend into its content. */
1928
+ if (isElement) {
1929
+ const rootName = getNodeName ? getNodeName(node) : null;
1930
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
1931
+ const content = node.content;
1932
+ if (_isDocumentFragment(content)) {
1933
+ stack.push({
1934
+ node: content,
1935
+ shadow: null
1936
+ });
1937
+ }
1938
+ }
1939
+ }
1940
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
1941
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
1942
+ rather than `instanceof DocumentFragment`, which is realm-bound and
1943
+ silently skipped foreign-realm shadow roots (e.g.
1944
+ iframe.contentDocument attachShadow). */
1945
+ if (isElement) {
1946
+ const sr = getShadowRoot(node);
1947
+ if (_isDocumentFragment(sr)) {
1948
+ /* Push the deferred sanitise first so it pops after the shadow
1949
+ walk we push next, i.e. nested shadow roots are discovered
1950
+ before this one is sanitised. */
1951
+ stack.push({
1952
+ node: null,
1953
+ shadow: sr
1954
+ }, {
1955
+ node: sr,
1956
+ shadow: null
1957
+ });
1958
+ }
1959
+ }
1348
1960
  }
1349
1961
  };
1350
1962
  // eslint-disable-next-line complexity
@@ -1373,28 +1985,78 @@ function createDOMPurify() {
1373
1985
  return dirty;
1374
1986
  }
1375
1987
  /* Assign config vars */
1376
- if (!SET_CONFIG) {
1988
+ if (SET_CONFIG) {
1989
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
1990
+ * not re-derived per call. Restore them from the pristine bindings
1991
+ * captured at setConfig() time so a previous call's hook clone (mutated
1992
+ * below) does not carry over. */
1993
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
1994
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
1995
+ } else {
1377
1996
  _parseConfig(cfg);
1378
1997
  }
1998
+ /* Clone the hook-mutable allowlists before the walk whenever an
1999
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2000
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2001
+ * a hook that widens them would otherwise mutate the shared set
2002
+ * permanently: across later calls and across every element. Cloning per
2003
+ * walk keeps documented in-call widening working while scoping it to the
2004
+ * call. A single guard for both config paths - the per-call path rebinds
2005
+ * the sets in _parseConfig each call, the persistent path restores them
2006
+ * from the captured bindings just above - so the two cannot diverge. */
2007
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
2008
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2009
+ }
2010
+ if (hooks.uponSanitizeAttribute.length > 0) {
2011
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2012
+ }
1379
2013
  /* Clean up removed elements */
1380
2014
  DOMPurify.removed = [];
1381
- /* Check if dirty is correctly typed for IN_PLACE */
1382
- if (typeof dirty === 'string') {
1383
- IN_PLACE = false;
1384
- }
1385
- if (IN_PLACE) {
1386
- /* Do some early pre-sanitization to avoid unsafe root nodes */
1387
- const nn = dirty.nodeName;
2015
+ /* Resolve IN_PLACE for this call without mutating persistent config.
2016
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
2017
+ where _parseConfig is skipped on later calls: a single string call would
2018
+ disable in-place mode for every subsequent node call, returning a
2019
+ sanitized copy while leaving the caller's node — which in-place callers
2020
+ keep using and whose return value they ignore unsanitized. REPORT-2. */
2021
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2022
+ if (inPlace) {
2023
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
2024
+ Read nodeName through the cached prototype getter — a clobbering
2025
+ child named "nodeName" on the form root would otherwise shadow
2026
+ the property and let this check skip the root-allowlist
2027
+ validation entirely. */
2028
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
1388
2029
  if (typeof nn === 'string') {
1389
2030
  const tagName = transformCaseFunc(nn);
1390
2031
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
1391
2032
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1392
2033
  }
1393
2034
  }
2035
+ /* Pre-flight the root through _isClobbered. The iterator-driven
2036
+ removal path can not detach a parent-less root: _forceRemove
2037
+ falls through to Element.prototype.remove(), which per spec
2038
+ is a no-op on a node with no parent. A clobbered root would
2039
+ then survive the main loop with its attributes uninspected,
2040
+ because _sanitizeAttributes early-returns on _isClobbered. The
2041
+ result would be an attacker-controlled form, complete with any
2042
+ event-handler attributes the caller passed in, handed back to
2043
+ the application unsanitized. Refuse to sanitize such a root
2044
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2045
+ if (_isClobbered(dirty)) {
2046
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
2047
+ }
1394
2048
  /* Sanitize attached shadow roots before the main iterator runs.
1395
- The iterator does not descend into shadow trees. */
1396
- _sanitizeAttachedShadowRoots2(dirty);
1397
- } else if (dirty instanceof Node) {
2049
+ The iterator does not descend into shadow trees. Same fail-closed
2050
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
2051
+ inside a shadow root could abort this pre-pass before the walk runs,
2052
+ which would otherwise leave the entire live tree unsanitized. */
2053
+ try {
2054
+ _sanitizeAttachedShadowRoots(dirty);
2055
+ } catch (error) {
2056
+ _neutralizeRoot(dirty);
2057
+ throw error;
2058
+ }
2059
+ } else if (_isNode(dirty)) {
1398
2060
  /* If dirty is a DOM element, append to an empty document to avoid
1399
2061
  elements being stripped by the parser */
1400
2062
  body = _initDocument('<!---->');
@@ -1410,14 +2072,16 @@ function createDOMPurify() {
1410
2072
  }
1411
2073
  /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1412
2074
  them before the main iterator runs, since the iterator does not
1413
- descend into shadow trees. */
1414
- _sanitizeAttachedShadowRoots2(importedNode);
2075
+ descend into shadow trees. The walk routes every read through a
2076
+ cached prototype getter so clobbering descendants on a form root
2077
+ cannot hide a shadow host from this pass. */
2078
+ _sanitizeAttachedShadowRoots(importedNode);
1415
2079
  } else {
1416
2080
  /* Exit directly if we have nothing to do */
1417
2081
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
1418
2082
  // eslint-disable-next-line unicorn/prefer-includes
1419
2083
  dirty.indexOf('<') === -1) {
1420
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
2084
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
1421
2085
  }
1422
2086
  /* Initialize the document to work on */
1423
2087
  body = _initDocument(dirty);
@@ -1431,31 +2095,59 @@ function createDOMPurify() {
1431
2095
  _forceRemove(body.firstChild);
1432
2096
  }
1433
2097
  /* Get node iterator */
1434
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
1435
- /* Now start iterating over the created document */
1436
- while (currentNode = nodeIterator.nextNode()) {
1437
- /* Sanitize tags and elements */
1438
- _sanitizeElements(currentNode);
1439
- /* Check attributes next */
1440
- _sanitizeAttributes(currentNode);
1441
- /* Shadow DOM detected, sanitize it */
1442
- if (currentNode.content instanceof DocumentFragment) {
1443
- _sanitizeShadowDOM2(currentNode.content);
2098
+ const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
2099
+ /* Now start iterating over the created document.
2100
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2101
+ engine/custom-element mutation can detach a node mid-walk so
2102
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2103
+ barrier the caller's in-place tree would be left half-sanitized with the
2104
+ unvisited tail still armed. On any throw we fail closed — strip the
2105
+ in-place root bare then rethrow so the existing throw contract is
2106
+ preserved. (String/DOM-copy paths never return the partial body, so the
2107
+ propagating throw is already fail-closed there.) */
2108
+ try {
2109
+ while (currentNode = nodeIterator.nextNode()) {
2110
+ /* Sanitize tags and elements */
2111
+ _sanitizeElements(currentNode);
2112
+ /* Check attributes next */
2113
+ _sanitizeAttributes(currentNode);
2114
+ /* Shadow DOM detected, sanitize it.
2115
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
2116
+ instead of instanceof, so foreign-realm <template>.content is
2117
+ walked correctly. */
2118
+ if (_isDocumentFragment(currentNode.content)) {
2119
+ _sanitizeShadowDOM2(currentNode.content);
2120
+ }
2121
+ }
2122
+ } catch (error) {
2123
+ if (inPlace) {
2124
+ _neutralizeRoot(dirty);
1444
2125
  }
2126
+ throw error;
1445
2127
  }
1446
2128
  /* If we sanitized `dirty` in-place, return it. */
1447
- if (IN_PLACE) {
2129
+ if (inPlace) {
2130
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
2131
+ the caller's live tree is detached but may still hold a queued
2132
+ resource-event handler that fires in page scope after we return. The
2133
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
2134
+ non-allow-listed attributes off every other removed subtree (clobber,
2135
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
2136
+ cancelled before any event can fire. Runs synchronously, pre-return. */
2137
+ arrayForEach(DOMPurify.removed, entry => {
2138
+ if (entry.element) {
2139
+ _neutralizeSubtree(entry.element);
2140
+ }
2141
+ });
2142
+ if (SAFE_FOR_TEMPLATES) {
2143
+ _scrubTemplateExpressions2(dirty);
2144
+ }
1448
2145
  return dirty;
1449
2146
  }
1450
2147
  /* Return sanitized string or DOM */
1451
2148
  if (RETURN_DOM) {
1452
2149
  if (SAFE_FOR_TEMPLATES) {
1453
- body.normalize();
1454
- let html = body.innerHTML;
1455
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1456
- html = stringReplace(html, expr, ' ');
1457
- });
1458
- body.innerHTML = html;
2150
+ _scrubTemplateExpressions2(body);
1459
2151
  }
1460
2152
  if (RETURN_DOM_FRAGMENT) {
1461
2153
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1485,20 +2177,28 @@ function createDOMPurify() {
1485
2177
  }
1486
2178
  /* Sanitize final string template-safe */
1487
2179
  if (SAFE_FOR_TEMPLATES) {
1488
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1489
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
1490
- });
2180
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
1491
2181
  }
1492
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
2182
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
1493
2183
  };
1494
2184
  DOMPurify.setConfig = function () {
1495
2185
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1496
2186
  _parseConfig(cfg);
1497
2187
  SET_CONFIG = true;
2188
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2189
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
1498
2190
  };
1499
2191
  DOMPurify.clearConfig = function () {
1500
2192
  CONFIG = null;
1501
2193
  SET_CONFIG = false;
2194
+ SET_CONFIG_ALLOWED_TAGS = null;
2195
+ SET_CONFIG_ALLOWED_ATTR = null;
2196
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
2197
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
2198
+ // never recreated — Trusted Types throws on duplicate names) is restored by
2199
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
2200
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
2201
+ emptyHTML = '';
1502
2202
  };
1503
2203
  DOMPurify.isValidAttribute = function (tag, attr, value) {
1504
2204
  /* Initialize shared config vars if necessary. */
@@ -1513,9 +2213,19 @@ function createDOMPurify() {
1513
2213
  if (typeof hookFunction !== 'function') {
1514
2214
  return;
1515
2215
  }
2216
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2217
+ * '__proto__') indexes off the prototype chain rather than a real
2218
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2219
+ * with an own-property check against the known hook names. */
2220
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2221
+ return;
2222
+ }
1516
2223
  arrayPush(hooks[entryPoint], hookFunction);
1517
2224
  };
1518
2225
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
2226
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2227
+ return undefined;
2228
+ }
1519
2229
  if (hookFunction !== undefined) {
1520
2230
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
1521
2231
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -1523,6 +2233,9 @@ function createDOMPurify() {
1523
2233
  return arrayPop(hooks[entryPoint]);
1524
2234
  };
1525
2235
  DOMPurify.removeHooks = function (entryPoint) {
2236
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2237
+ return;
2238
+ }
1526
2239
  hooks[entryPoint] = [];
1527
2240
  };
1528
2241
  DOMPurify.removeAllHooks = function () {