dompurify 3.4.2 → 3.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,62 @@
1
- /*! @license DOMPurify 3.4.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.2/LICENSE */
1
+ /*! @license DOMPurify 3.4.4 | (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.4/LICENSE */
2
2
 
3
- const {
4
- entries,
5
- setPrototypeOf,
6
- isFrozen,
7
- getPrototypeOf,
8
- getOwnPropertyDescriptor
9
- } = Object;
10
- let {
11
- freeze,
12
- seal,
13
- create
14
- } = Object; // eslint-disable-line import/no-mutable-exports
15
- let {
16
- apply,
17
- construct
18
- } = typeof Reflect !== 'undefined' && Reflect;
3
+ function _arrayLikeToArray(r, a) {
4
+ (null == a || a > r.length) && (a = r.length);
5
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
6
+ return n;
7
+ }
8
+ function _arrayWithHoles(r) {
9
+ if (Array.isArray(r)) return r;
10
+ }
11
+ function _iterableToArrayLimit(r, l) {
12
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
13
+ if (null != t) {
14
+ var e,
15
+ n,
16
+ i,
17
+ u,
18
+ a = [],
19
+ f = true,
20
+ o = false;
21
+ try {
22
+ if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
23
+ } catch (r) {
24
+ o = true, n = r;
25
+ } finally {
26
+ try {
27
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
28
+ } finally {
29
+ if (o) throw n;
30
+ }
31
+ }
32
+ return a;
33
+ }
34
+ }
35
+ function _nonIterableRest() {
36
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
37
+ }
38
+ function _slicedToArray(r, e) {
39
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
40
+ }
41
+ function _unsupportedIterableToArray(r, a) {
42
+ if (r) {
43
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
44
+ var t = {}.toString.call(r).slice(8, -1);
45
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
46
+ }
47
+ }
48
+
49
+ const entries = Object.entries,
50
+ setPrototypeOf = Object.setPrototypeOf,
51
+ isFrozen = Object.isFrozen,
52
+ getPrototypeOf = Object.getPrototypeOf,
53
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
54
+ let freeze = Object.freeze,
55
+ seal = Object.seal,
56
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
57
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
58
+ apply = _ref.apply,
59
+ construct = _ref.construct;
19
60
  if (!freeze) {
20
61
  freeze = function freeze(x) {
21
62
  return x;
@@ -152,7 +193,10 @@ function cleanArray(array) {
152
193
  */
153
194
  function clone(object) {
154
195
  const newObject = create(null);
155
- for (const [property, value] of entries(object)) {
196
+ for (const _ref2 of entries(object)) {
197
+ var _ref3 = _slicedToArray(_ref2, 2);
198
+ const property = _ref3[0];
199
+ const value = _ref3[1];
156
200
  const isPropertyExist = objectHasOwnProperty(object, property);
157
201
  if (isPropertyExist) {
158
202
  if (arrayIsArray(value)) {
@@ -252,7 +296,7 @@ function isRegex(value) {
252
296
  }
253
297
  }
254
298
 
255
- const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
299
+ const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
256
300
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
257
301
  const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
258
302
  // List of SVG elements that are disallowed by default.
@@ -266,15 +310,14 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
266
310
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
267
311
  const text = freeze(['#text']);
268
312
 
269
- 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']);
270
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']);
271
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']);
272
316
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
273
317
 
274
- // eslint-disable-next-line unicorn/better-regex
275
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
276
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
277
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
318
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
319
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
320
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
278
321
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
279
322
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
280
323
  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
@@ -285,20 +328,6 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
285
328
  const DOCTYPE_NAME = seal(/^html$/i);
286
329
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
287
330
 
288
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
289
- __proto__: null,
290
- ARIA_ATTR: ARIA_ATTR,
291
- ATTR_WHITESPACE: ATTR_WHITESPACE,
292
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
293
- DATA_ATTR: DATA_ATTR,
294
- DOCTYPE_NAME: DOCTYPE_NAME,
295
- ERB_EXPR: ERB_EXPR,
296
- IS_ALLOWED_URI: IS_ALLOWED_URI,
297
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
298
- MUSTACHE_EXPR: MUSTACHE_EXPR,
299
- TMPLIT_EXPR: TMPLIT_EXPR
300
- });
301
-
302
331
  /* eslint-disable @typescript-eslint/indent */
303
332
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
304
333
  const NODE_TYPE = {
@@ -365,7 +394,7 @@ const _createHooksMap = function _createHooksMap() {
365
394
  function createDOMPurify() {
366
395
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
367
396
  const DOMPurify = root => createDOMPurify(root);
368
- DOMPurify.version = '3.4.2';
397
+ DOMPurify.version = '3.4.4';
369
398
  DOMPurify.removed = [];
370
399
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
371
400
  // Not running in a browser, provide a factory function
@@ -373,28 +402,26 @@ function createDOMPurify() {
373
402
  DOMPurify.isSupported = false;
374
403
  return DOMPurify;
375
404
  }
376
- let {
377
- document
378
- } = window;
405
+ let document = window.document;
379
406
  const originalDocument = document;
380
407
  const currentScript = originalDocument.currentScript;
381
- const {
382
- DocumentFragment,
383
- HTMLTemplateElement,
384
- Node,
385
- Element,
386
- NodeFilter,
387
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
388
- HTMLFormElement,
389
- DOMParser,
390
- trustedTypes
391
- } = window;
408
+ const DocumentFragment = window.DocumentFragment,
409
+ HTMLTemplateElement = window.HTMLTemplateElement,
410
+ Node = window.Node,
411
+ Element = window.Element,
412
+ 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,
417
+ trustedTypes = window.trustedTypes;
392
418
  const ElementPrototype = Element.prototype;
393
419
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
394
420
  const remove = lookupGetter(ElementPrototype, 'remove');
395
421
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
396
422
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
397
423
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
424
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
398
425
  // As per issue #47, the web-components registry is inherited by a
399
426
  // new document created via createHTMLDocument. As per the spec
400
427
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -409,33 +436,26 @@ function createDOMPurify() {
409
436
  }
410
437
  let trustedTypesPolicy;
411
438
  let emptyHTML = '';
412
- const {
413
- implementation,
414
- createNodeIterator,
415
- createDocumentFragment,
416
- getElementsByTagName
417
- } = document;
418
- const {
419
- importNode
420
- } = originalDocument;
439
+ const _document = document,
440
+ implementation = _document.implementation,
441
+ createNodeIterator = _document.createNodeIterator,
442
+ createDocumentFragment = _document.createDocumentFragment,
443
+ getElementsByTagName = _document.getElementsByTagName;
444
+ const importNode = originalDocument.importNode;
421
445
  let hooks = _createHooksMap();
422
446
  /**
423
447
  * Expose whether this browser supports running the full DOMPurify.
424
448
  */
425
449
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
426
- const {
427
- MUSTACHE_EXPR,
428
- ERB_EXPR,
429
- TMPLIT_EXPR,
430
- DATA_ATTR,
431
- ARIA_ATTR,
432
- IS_SCRIPT_OR_DATA,
433
- ATTR_WHITESPACE,
434
- CUSTOM_ELEMENT
435
- } = EXPRESSIONS;
436
- let {
437
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
438
- } = EXPRESSIONS;
450
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
451
+ ERB_EXPR$1 = ERB_EXPR,
452
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
453
+ DATA_ATTR$1 = DATA_ATTR,
454
+ ARIA_ATTR$1 = ARIA_ATTR,
455
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
456
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
457
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
458
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
439
459
  /**
440
460
  * We consider the elements and attributes below to be safe. Ideally
441
461
  * don't add any new ones but feel free to remove unwanted ones.
@@ -955,6 +975,40 @@ function createDOMPurify() {
955
975
  // eslint-disable-next-line no-bitwise
956
976
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
957
977
  };
978
+ /**
979
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
980
+ * character data of an element subtree. Used as the final safety net for
981
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
982
+ * which only form after text-node normalization (e.g. fragments split across
983
+ * stripped elements) cannot survive into a template-evaluating framework.
984
+ *
985
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
986
+ * in place rather than round-tripping through innerHTML. This preserves
987
+ * descendant node references (important for IN_PLACE callers), avoids a
988
+ * serialize/reparse cycle, and reads literal character data — which means
989
+ * `<%...%>` in text content matches the ERB regex against its real bytes
990
+ * instead of the HTML-entity-escaped form innerHTML would produce.
991
+ *
992
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
993
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
994
+ *
995
+ * @param node The root element whose character data should be scrubbed.
996
+ */
997
+ const _scrubTemplateExpressions = function _scrubTemplateExpressions(node) {
998
+ node.normalize();
999
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
1000
+ // eslint-disable-next-line no-bitwise
1001
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1002
+ let currentNode = walker.nextNode();
1003
+ while (currentNode) {
1004
+ let data = currentNode.data;
1005
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1006
+ data = stringReplace(data, expr, ' ');
1007
+ });
1008
+ currentNode.data = data;
1009
+ currentNode = walker.nextNode();
1010
+ }
1011
+ };
958
1012
  /**
959
1013
  * _isClobbered
960
1014
  *
@@ -965,13 +1019,31 @@ function createDOMPurify() {
965
1019
  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');
966
1020
  };
967
1021
  /**
968
- * Checks whether the given object is a DOM node.
1022
+ * Checks whether the given object is a DOM node, including nodes that
1023
+ * originate from a different window/realm (e.g. an iframe's
1024
+ * contentDocument). The previous `value instanceof Node` check was
1025
+ * realm-bound: nodes from a different window failed it, causing
1026
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
1027
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
1028
+ *
1029
+ * Implementation: call the cached `nodeType` getter from Node.prototype
1030
+ * directly on the value. This bypasses any clobbered instance property
1031
+ * (e.g. a child element named "nodeType") and works across realms
1032
+ * because the WebIDL `nodeType` getter reads an internal slot that
1033
+ * every real Node has, regardless of which window minted it.
969
1034
  *
970
1035
  * @param value object to check whether it's a DOM node
971
- * @return true is object is a DOM node
1036
+ * @return true if value is a DOM node from any realm
972
1037
  */
973
1038
  const _isNode = function _isNode(value) {
974
- return typeof Node === 'function' && value instanceof Node;
1039
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1040
+ return false;
1041
+ }
1042
+ try {
1043
+ return typeof getNodeType(value) === 'number';
1044
+ } catch (_) {
1045
+ return false;
1046
+ }
975
1047
  };
976
1048
  function _executeHooks(hooks, currentNode, data) {
977
1049
  arrayForEach(hooks, hook => {
@@ -1063,7 +1135,7 @@ function createDOMPurify() {
1063
1135
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1064
1136
  /* Get the element's text content */
1065
1137
  content = currentNode.textContent;
1066
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1138
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1067
1139
  content = stringReplace(content, expr, ' ');
1068
1140
  });
1069
1141
  if (currentNode.textContent !== content) {
@@ -1100,7 +1172,7 @@ function createDOMPurify() {
1100
1172
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1101
1173
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1102
1174
  We don't need to check the value; it's always URI safe. */
1103
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1175
+ 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]) {
1104
1176
  if (
1105
1177
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1106
1178
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1112,7 +1184,7 @@ function createDOMPurify() {
1112
1184
  return false;
1113
1185
  }
1114
1186
  /* Check value is safe. First, is attr inert? If so, is safe */
1115
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
1187
+ } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
1116
1188
  return false;
1117
1189
  } else ;
1118
1190
  return true;
@@ -1130,7 +1202,7 @@ function createDOMPurify() {
1130
1202
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
1131
1203
  */
1132
1204
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1133
- return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT, tagName);
1205
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1134
1206
  };
1135
1207
  /**
1136
1208
  * _sanitizeAttributes
@@ -1145,9 +1217,7 @@ function createDOMPurify() {
1145
1217
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
1146
1218
  /* Execute a hook if present */
1147
1219
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
1148
- const {
1149
- attributes
1150
- } = currentNode;
1220
+ const attributes = currentNode.attributes;
1151
1221
  /* Check if we have attributes; if not we might have a text node */
1152
1222
  if (!attributes || _isClobbered(currentNode)) {
1153
1223
  return;
@@ -1163,11 +1233,9 @@ function createDOMPurify() {
1163
1233
  /* Go backwards over all attributes; safely remove bad ones */
1164
1234
  while (l--) {
1165
1235
  const attr = attributes[l];
1166
- const {
1167
- name,
1168
- namespaceURI,
1169
- value: attrValue
1170
- } = attr;
1236
+ const name = attr.name,
1237
+ namespaceURI = attr.namespaceURI,
1238
+ attrValue = attr.value;
1171
1239
  const lcName = transformCaseFunc(name);
1172
1240
  const initValue = attrValue;
1173
1241
  let value = name === 'value' ? initValue : stringTrim(initValue);
@@ -1215,7 +1283,7 @@ function createDOMPurify() {
1215
1283
  }
1216
1284
  /* Sanitize attribute content to be template-safe */
1217
1285
  if (SAFE_FOR_TEMPLATES) {
1218
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1286
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1219
1287
  value = stringReplace(value, expr, ' ');
1220
1288
  });
1221
1289
  }
@@ -1289,6 +1357,49 @@ function createDOMPurify() {
1289
1357
  /* Execute a hook if present */
1290
1358
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
1291
1359
  };
1360
+ /**
1361
+ * _sanitizeAttachedShadowRoots
1362
+ *
1363
+ * Walks `root` and feeds every attached shadow root we encounter into
1364
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
1365
+ * does not descend into shadow trees, so nodes inside an attached
1366
+ * shadow root would otherwise be skipped entirely.
1367
+ *
1368
+ * Two real input paths put attached shadow roots in front of us:
1369
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
1370
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
1371
+ * shadow root because it was created with `clonable: true`.
1372
+ *
1373
+ * This pass runs once, up front, so the main iteration loop (and the
1374
+ * existing _sanitizeShadowDOM template-content recursion) stay
1375
+ * untouched — string-input paths are not affected.
1376
+ *
1377
+ * @param root the subtree root to walk for attached shadow roots
1378
+ */
1379
+ const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1380
+ if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
1381
+ const sr = root.shadowRoot;
1382
+ // Recurse first so that nested shadow roots are reached even if
1383
+ // _sanitizeShadowDOM removes hosts at this level.
1384
+ _sanitizeAttachedShadowRoots2(sr);
1385
+ _sanitizeShadowDOM2(sr);
1386
+ }
1387
+ // Snapshot children before recursing. Sanitization of one subtree
1388
+ // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1389
+ // and naive nextSibling traversal would silently skip the rest of
1390
+ // the list once a node is detached.
1391
+ const childNodes = root.childNodes;
1392
+ if (!childNodes) {
1393
+ return;
1394
+ }
1395
+ const snapshot = [];
1396
+ arrayForEach(childNodes, child => {
1397
+ arrayPush(snapshot, child);
1398
+ });
1399
+ for (const child of snapshot) {
1400
+ _sanitizeAttachedShadowRoots2(child);
1401
+ }
1402
+ };
1292
1403
  // eslint-disable-next-line complexity
1293
1404
  DOMPurify.sanitize = function (dirty) {
1294
1405
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -1333,7 +1444,10 @@ function createDOMPurify() {
1333
1444
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1334
1445
  }
1335
1446
  }
1336
- } else if (dirty instanceof Node) {
1447
+ /* Sanitize attached shadow roots before the main iterator runs.
1448
+ The iterator does not descend into shadow trees. */
1449
+ _sanitizeAttachedShadowRoots2(dirty);
1450
+ } else if (_isNode(dirty)) {
1337
1451
  /* If dirty is a DOM element, append to an empty document to avoid
1338
1452
  elements being stripped by the parser */
1339
1453
  body = _initDocument('<!---->');
@@ -1347,6 +1461,10 @@ function createDOMPurify() {
1347
1461
  // eslint-disable-next-line unicorn/prefer-dom-node-append
1348
1462
  body.appendChild(importedNode);
1349
1463
  }
1464
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1465
+ them before the main iterator runs, since the iterator does not
1466
+ descend into shadow trees. */
1467
+ _sanitizeAttachedShadowRoots2(importedNode);
1350
1468
  } else {
1351
1469
  /* Exit directly if we have nothing to do */
1352
1470
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
@@ -1380,17 +1498,15 @@ function createDOMPurify() {
1380
1498
  }
1381
1499
  /* If we sanitized `dirty` in-place, return it. */
1382
1500
  if (IN_PLACE) {
1501
+ if (SAFE_FOR_TEMPLATES) {
1502
+ _scrubTemplateExpressions(dirty);
1503
+ }
1383
1504
  return dirty;
1384
1505
  }
1385
1506
  /* Return sanitized string or DOM */
1386
1507
  if (RETURN_DOM) {
1387
1508
  if (SAFE_FOR_TEMPLATES) {
1388
- body.normalize();
1389
- let html = body.innerHTML;
1390
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1391
- html = stringReplace(html, expr, ' ');
1392
- });
1393
- body.innerHTML = html;
1509
+ _scrubTemplateExpressions(body);
1394
1510
  }
1395
1511
  if (RETURN_DOM_FRAGMENT) {
1396
1512
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1420,7 +1536,7 @@ function createDOMPurify() {
1420
1536
  }
1421
1537
  /* Sanitize final string template-safe */
1422
1538
  if (SAFE_FOR_TEMPLATES) {
1423
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
1539
+ arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1424
1540
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1425
1541
  });
1426
1542
  }