@lwrjs/client-modules 0.23.7 → 0.23.11

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.
@@ -1347,6 +1347,22 @@ function consoleWarn$LWS(...args$LWS) {
1347
1347
  const gaterEnabledFeatures$LWS = toSafeSet$LWS(new SetCtor$LWS$1());
1348
1348
  const FEATURE_NAME_PREFIX$LWS = 'com.salesforce.locker.';
1349
1349
  const fullName$LWS = featureName$LWS => featureName$LWS.startsWith(FEATURE_NAME_PREFIX$LWS) ? featureName$LWS : `${FEATURE_NAME_PREFIX$LWS}${featureName$LWS}`;
1350
+ // The canonical set of LWS feature gates that are enabled in the test
1351
+ // harness (karma), and therefore in CI. This is the single source of truth
1352
+ // for "what gates a faithful LWS run turns on". Consumers register it as
1353
+ // their baseline so they reproduce CI's gate state: the karma evaluator
1354
+ // registers exactly this list before running tests, and the @locker/cli
1355
+ // tool registers it as the default for a `lws` run. Adding a gate here
1356
+ // turns it on by default in every consumer that registers this list.
1357
+ //
1358
+ // This is an inert exported constant, never auto-registered at module
1359
+ // load: registering here would change behavior for every @locker/shared
1360
+ // consumer. Each consumer opts in by passing it to
1361
+ // registerGaterEnabledFeatures. Names are fully qualified so the value
1362
+ // reads identically to what isGaterEnabledFeature stores (register and
1363
+ // deregister also accept the short form via fullName above). Frozen so a
1364
+ // consumer cannot mutate the shared array.
1365
+ Object.freeze([`${FEATURE_NAME_PREFIX$LWS}changesSince.256`, `${FEATURE_NAME_PREFIX$LWS}changesSince.258`, `${FEATURE_NAME_PREFIX$LWS}changesSince.260`, `${FEATURE_NAME_PREFIX$LWS}changesSince.262`, `${FEATURE_NAME_PREFIX$LWS}changesSince.264`, `${FEATURE_NAME_PREFIX$LWS}htmlAnchorElementHrefValidation`, `${FEATURE_NAME_PREFIX$LWS}enableSandboxedSameOriginIframe`, `${FEATURE_NAME_PREFIX$LWS}xmlEntityAttackPrologScan`]);
1350
1366
  function isGaterEnabledFeature$LWS(featureName$LWS) {
1351
1367
  return gaterEnabledFeatures$LWS.has(fullName$LWS(featureName$LWS));
1352
1368
  }
@@ -1511,7 +1527,7 @@ const {
1511
1527
  const PromiseResolve$LWS = PromiseCtor$LWS.resolve.bind(PromiseCtor$LWS);
1512
1528
  const PromiseReject$LWS = PromiseCtor$LWS.reject.bind(PromiseCtor$LWS);
1513
1529
  const trustedResources$LWS = toSafeSet$LWS(new SetCtor$LWS$1());
1514
- /*! version: 0.28.3 */
1530
+ /*! version: 0.28.4 */
1515
1531
 
1516
1532
  /*!
1517
1533
  * Copyright (C) 2019 salesforce.com, inc.
@@ -1979,6 +1995,69 @@ const DOCTYPE_WITH_INTERNAL_SUBSET_REGEX$LWS = /<!doctype\s+\S+\s*\[/;
1979
1995
  // Matches: <!DOCTYPE root SYSTEM "..."> or <!DOCTYPE root PUBLIC "..." "...">
1980
1996
  // External DTDs can define entities that get expanded with malicious content.
1981
1997
  const DOCTYPE_EXTERNAL_ENTITY_REGEX$LWS = /<!doctype\s+\S+\s+(?:system|public)\s/;
1998
+ const DOCTYPE_OPENER$LWS = '<!doctype';
1999
+ const PI_OPENER$LWS = '<?';
2000
+ const PI_CLOSER$LWS = '?>';
2001
+ const COMMENT_OPENER$LWS = '<!--';
2002
+ const COMMENT_CLOSER$LWS = '-->';
2003
+ // Locate a DOCTYPE that sits in the document prolog — the only position where a
2004
+ // DTD is actually parsed and where entity definitions can take effect. A DTD is
2005
+ // only meaningful before the root element's start tag; anything after the root
2006
+ // element opens (text, CDATA sections, comments, processing instructions) is
2007
+ // inert character data and can never act as a DTD, no matter what it spells.
2008
+ //
2009
+ // Returns the prolog slice beginning at the DOCTYPE so the existing
2010
+ // dangerous-form regexes can classify it, or null when the prolog contains no
2011
+ // DOCTYPE. Walks the prolog token by token, skipping the XML declaration and
2012
+ // processing instructions (`<?...?>`) and comments (`<!-- ... -->`), and stops
2013
+ // at the first other `<` — that is the root element's start tag. Skipping
2014
+ // comments here is what defeats the `<!-- <![CDATA[ -->` decoy: a real prolog
2015
+ // DOCTYPE after a comment is still found, and a DOCTYPE buried inside a comment
2016
+ // is correctly ignored.
2017
+ function findPrologDoctype$LWS(normalizedInput$LWS) {
2018
+ const {
2019
+ length: length$LWS
2020
+ } = normalizedInput$LWS;
2021
+ let index$LWS = 0;
2022
+ while (index$LWS < length$LWS) {
2023
+ const char$LWS = ReflectApply$LWS$1(StringProtoCharAt$LWS, normalizedInput$LWS, [index$LWS]);
2024
+ // Skip insignificant whitespace between prolog tokens.
2025
+ if (char$LWS === ' ' || char$LWS === '\t' || char$LWS === '\n' || char$LWS === '\r') {
2026
+ index$LWS += 1;
2027
+ continue;
2028
+ }
2029
+ // Anything that is not a markup-opening `<` at this point is either the
2030
+ // start of the root element's content model or malformed; in both cases
2031
+ // there is no prolog DOCTYPE to find.
2032
+ if (char$LWS !== '<') {
2033
+ return null;
2034
+ }
2035
+ if (ReflectApply$LWS$1(StringProtoStartsWith$LWS, normalizedInput$LWS, [DOCTYPE_OPENER$LWS, index$LWS])) {
2036
+ return ReflectApply$LWS$1(StringProtoSlice$LWS$1, normalizedInput$LWS, [index$LWS]);
2037
+ }
2038
+ if (ReflectApply$LWS$1(StringProtoStartsWith$LWS, normalizedInput$LWS, [COMMENT_OPENER$LWS, index$LWS])) {
2039
+ const closerOffset$LWS = ReflectApply$LWS$1(StringProtoIndexOf$LWS, normalizedInput$LWS, [COMMENT_CLOSER$LWS, index$LWS + COMMENT_OPENER$LWS.length]);
2040
+ // Unterminated comment: no well-formed prolog DOCTYPE can follow.
2041
+ if (closerOffset$LWS === -1) {
2042
+ return null;
2043
+ }
2044
+ index$LWS = closerOffset$LWS + COMMENT_CLOSER$LWS.length;
2045
+ continue;
2046
+ }
2047
+ if (ReflectApply$LWS$1(StringProtoStartsWith$LWS, normalizedInput$LWS, [PI_OPENER$LWS, index$LWS])) {
2048
+ const closerOffset$LWS = ReflectApply$LWS$1(StringProtoIndexOf$LWS, normalizedInput$LWS, [PI_CLOSER$LWS, index$LWS + PI_OPENER$LWS.length]);
2049
+ if (closerOffset$LWS === -1) {
2050
+ return null;
2051
+ }
2052
+ index$LWS = closerOffset$LWS + PI_CLOSER$LWS.length;
2053
+ continue;
2054
+ }
2055
+ // First `<` that is not a PI, comment, or DOCTYPE is the root element
2056
+ // start tag. The prolog has ended without a DOCTYPE.
2057
+ return null;
2058
+ }
2059
+ return null;
2060
+ }
1982
2061
  /* eslint-disable no-underscore-dangle */
1983
2062
  class Validator$LWS {
1984
2063
  constructor(document$LWS, {
@@ -1995,12 +2074,28 @@ class Validator$LWS {
1995
2074
  // Detect XML External Entity (XXE) injection attacks via DOCTYPE declarations.
1996
2075
  // Attackers can embed malicious script content inside ENTITY definitions that
1997
2076
  // get expanded when the XML/SVG is rendered, bypassing DOMPurify sanitization.
2077
+ //
2078
+ // A DTD only takes effect when it appears in the document prolog (before the
2079
+ // root element). The original guard scanned the entire input for a dangerous
2080
+ // DOCTYPE substring, which also rejected benign documents that merely quote a
2081
+ // DOCTYPE as inert text — inside a CDATA section, an XML comment, or element
2082
+ // text. The prolog-aware path keeps the dangerous-form classification
2083
+ // (internal subset, SYSTEM/PUBLIC external reference) exactly as-is, but only
2084
+ // applies it to a DOCTYPE that actually sits in the prolog. When the gate is
2085
+ // off, the original whole-input scan is preserved.
1998
2086
  // eslint-disable-next-line class-methods-use-this
1999
2087
  this.isXMLEntityAttack = input$LWS => {
2000
2088
  const normalizedInput$LWS = normalizeInput$LWS(input$LWS, ' ');
2001
2089
  // Block DOCTYPE with:
2002
2090
  // - Internal DTD subset (e.g., <!DOCTYPE svg [<!ENTITY foo "...">]>)
2003
2091
  // - SYSTEM or PUBLIC external entity references
2092
+ if (isGaterEnabledFeature$LWS('xmlEntityAttackPrologScan')) {
2093
+ const prologDoctype$LWS = findPrologDoctype$LWS(normalizedInput$LWS);
2094
+ if (prologDoctype$LWS === null) {
2095
+ return false;
2096
+ }
2097
+ return ReflectApply$LWS$1(RegExpProtoTest$LWS$1, DOCTYPE_WITH_INTERNAL_SUBSET_REGEX$LWS, [prologDoctype$LWS]) || ReflectApply$LWS$1(RegExpProtoTest$LWS$1, DOCTYPE_EXTERNAL_ENTITY_REGEX$LWS, [prologDoctype$LWS]);
2098
+ }
2004
2099
  return ReflectApply$LWS$1(RegExpProtoTest$LWS$1, DOCTYPE_WITH_INTERNAL_SUBSET_REGEX$LWS, [normalizedInput$LWS]) || ReflectApply$LWS$1(RegExpProtoTest$LWS$1, DOCTYPE_EXTERNAL_ENTITY_REGEX$LWS, [normalizedInput$LWS]);
2005
2100
  };
2006
2101
  // Detect namespaced script elements that execute in XML contexts.
@@ -2515,7 +2610,7 @@ const {
2515
2610
  const XhrProtoResponseTextGetter$LWS = ObjectLookupOwnGetter$LWS$1(XhrProto$LWS, 'responseText');
2516
2611
  const XhrProtoStatusGetter$LWS = ObjectLookupOwnGetter$LWS$1(XhrProto$LWS, 'status');
2517
2612
  ObjectLookupOwnSetter$LWS(XhrProto$LWS, 'withCredentials');
2518
- /*! version: 0.28.3 */
2613
+ /*! version: 0.28.4 */
2519
2614
 
2520
2615
  /*!
2521
2616
  * Copyright (C) 2019 salesforce.com, inc.
@@ -2683,26 +2778,67 @@ function sanitizeSrcsetAndValidateEndpoints$LWS(rawSrcset$LWS) {
2683
2778
  }
2684
2779
  return sanitized$LWS;
2685
2780
  }
2686
- /*! version: 0.28.3 */
2781
+ /*! version: 0.28.4 */
2687
2782
 
2688
- /*! @license DOMPurify 3.2.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.2.4/LICENSE */
2783
+ /*! @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 */
2689
2784
 
2690
- const {
2691
- entries,
2692
- setPrototypeOf,
2693
- isFrozen,
2694
- getPrototypeOf,
2695
- getOwnPropertyDescriptor
2696
- } = Object;
2697
- let {
2698
- freeze,
2699
- seal,
2700
- create
2701
- } = Object; // eslint-disable-line import/no-mutable-exports
2702
- let {
2703
- apply,
2704
- construct
2705
- } = typeof Reflect !== 'undefined' && Reflect;
2785
+ function _arrayLikeToArray(r, a) {
2786
+ (null == a || a > r.length) && (a = r.length);
2787
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
2788
+ return n;
2789
+ }
2790
+ function _arrayWithHoles(r) {
2791
+ if (Array.isArray(r)) return r;
2792
+ }
2793
+ function _iterableToArrayLimit(r, l) {
2794
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
2795
+ if (null != t) {
2796
+ var e,
2797
+ n,
2798
+ i,
2799
+ u,
2800
+ a = [],
2801
+ f = true,
2802
+ o = false;
2803
+ try {
2804
+ 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);
2805
+ } catch (r) {
2806
+ o = true, n = r;
2807
+ } finally {
2808
+ try {
2809
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
2810
+ } finally {
2811
+ if (o) throw n;
2812
+ }
2813
+ }
2814
+ return a;
2815
+ }
2816
+ }
2817
+ function _nonIterableRest() {
2818
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
2819
+ }
2820
+ function _slicedToArray(r, e) {
2821
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
2822
+ }
2823
+ function _unsupportedIterableToArray(r, a) {
2824
+ if (r) {
2825
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
2826
+ var t = {}.toString.call(r).slice(8, -1);
2827
+ 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;
2828
+ }
2829
+ }
2830
+
2831
+ const entries = Object.entries,
2832
+ setPrototypeOf = Object.setPrototypeOf,
2833
+ isFrozen = Object.isFrozen,
2834
+ getPrototypeOf = Object.getPrototypeOf,
2835
+ getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
2836
+ let freeze = Object.freeze,
2837
+ seal = Object.seal,
2838
+ create = Object.create; // eslint-disable-line import/no-mutable-exports
2839
+ let _ref = typeof Reflect !== 'undefined' && Reflect,
2840
+ apply = _ref.apply,
2841
+ construct = _ref.construct;
2706
2842
  if (!freeze) {
2707
2843
  freeze = function freeze(x) {
2708
2844
  return x;
@@ -2714,12 +2850,18 @@ if (!seal) {
2714
2850
  };
2715
2851
  }
2716
2852
  if (!apply) {
2717
- apply = function apply(fun, thisValue, args) {
2718
- return fun.apply(thisValue, args);
2853
+ apply = function apply(func, thisArg) {
2854
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
2855
+ args[_key - 2] = arguments[_key];
2856
+ }
2857
+ return func.apply(thisArg, args);
2719
2858
  };
2720
2859
  }
2721
2860
  if (!construct) {
2722
- construct = function construct(Func, args) {
2861
+ construct = function construct(Func) {
2862
+ for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
2863
+ args[_key2 - 1] = arguments[_key2];
2864
+ }
2723
2865
  return new Func(...args);
2724
2866
  };
2725
2867
  }
@@ -2728,13 +2870,19 @@ const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
2728
2870
  const arrayPop = unapply(Array.prototype.pop);
2729
2871
  const arrayPush = unapply(Array.prototype.push);
2730
2872
  const arraySplice = unapply(Array.prototype.splice);
2873
+ const arrayIsArray = Array.isArray;
2731
2874
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
2732
2875
  const stringToString = unapply(String.prototype.toString);
2733
2876
  const stringMatch = unapply(String.prototype.match);
2734
2877
  const stringReplace = unapply(String.prototype.replace);
2735
2878
  const stringIndexOf = unapply(String.prototype.indexOf);
2736
2879
  const stringTrim = unapply(String.prototype.trim);
2880
+ const numberToString = unapply(Number.prototype.toString);
2881
+ const booleanToString = unapply(Boolean.prototype.toString);
2882
+ const bigintToString = typeof BigInt === 'undefined' ? null : unapply(BigInt.prototype.toString);
2883
+ const symbolToString = typeof Symbol === 'undefined' ? null : unapply(Symbol.prototype.toString);
2737
2884
  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
2885
+ const objectToString = unapply(Object.prototype.toString);
2738
2886
  const regExpTest = unapply(RegExp.prototype.test);
2739
2887
  const typeErrorCreate = unconstruct(TypeError);
2740
2888
  /**
@@ -2745,8 +2893,11 @@ const typeErrorCreate = unconstruct(TypeError);
2745
2893
  */
2746
2894
  function unapply(func) {
2747
2895
  return function (thisArg) {
2748
- for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
2749
- args[_key - 1] = arguments[_key];
2896
+ if (thisArg instanceof RegExp) {
2897
+ thisArg.lastIndex = 0;
2898
+ }
2899
+ for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
2900
+ args[_key3 - 1] = arguments[_key3];
2750
2901
  }
2751
2902
  return apply(func, thisArg, args);
2752
2903
  };
@@ -2757,12 +2908,12 @@ function unapply(func) {
2757
2908
  * @param func - The constructor function to be wrapped and called.
2758
2909
  * @returns A new function that constructs an instance of the given constructor function with the provided arguments.
2759
2910
  */
2760
- function unconstruct(func) {
2911
+ function unconstruct(Func) {
2761
2912
  return function () {
2762
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
2763
- args[_key2] = arguments[_key2];
2913
+ for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
2914
+ args[_key4] = arguments[_key4];
2764
2915
  }
2765
- return construct(func, args);
2916
+ return construct(Func, args);
2766
2917
  };
2767
2918
  }
2768
2919
  /**
@@ -2781,6 +2932,9 @@ function addToSet(set, array) {
2781
2932
  // Prevent prototype setters from intercepting set as a this value.
2782
2933
  setPrototypeOf(set, null);
2783
2934
  }
2935
+ if (!arrayIsArray(array)) {
2936
+ return set;
2937
+ }
2784
2938
  let l = array.length;
2785
2939
  while (l--) {
2786
2940
  let element = array[l];
@@ -2821,10 +2975,13 @@ function cleanArray(array) {
2821
2975
  */
2822
2976
  function clone(object) {
2823
2977
  const newObject = create(null);
2824
- for (const [property, value] of entries(object)) {
2978
+ for (const _ref2 of entries(object)) {
2979
+ var _ref3 = _slicedToArray(_ref2, 2);
2980
+ const property = _ref3[0];
2981
+ const value = _ref3[1];
2825
2982
  const isPropertyExist = objectHasOwnProperty(object, property);
2826
2983
  if (isPropertyExist) {
2827
- if (Array.isArray(value)) {
2984
+ if (arrayIsArray(value)) {
2828
2985
  newObject[property] = cleanArray(value);
2829
2986
  } else if (value && typeof value === 'object' && value.constructor === Object) {
2830
2987
  newObject[property] = clone(value);
@@ -2835,6 +2992,58 @@ function clone(object) {
2835
2992
  }
2836
2993
  return newObject;
2837
2994
  }
2995
+ /**
2996
+ * Convert non-node values into strings without depending on direct property access.
2997
+ *
2998
+ * @param value - The value to stringify.
2999
+ * @returns A string representation of the provided value.
3000
+ */
3001
+ function stringifyValue(value) {
3002
+ switch (typeof value) {
3003
+ case 'string':
3004
+ {
3005
+ return value;
3006
+ }
3007
+ case 'number':
3008
+ {
3009
+ return numberToString(value);
3010
+ }
3011
+ case 'boolean':
3012
+ {
3013
+ return booleanToString(value);
3014
+ }
3015
+ case 'bigint':
3016
+ {
3017
+ return bigintToString ? bigintToString(value) : '0';
3018
+ }
3019
+ case 'symbol':
3020
+ {
3021
+ return symbolToString ? symbolToString(value) : 'Symbol()';
3022
+ }
3023
+ case 'undefined':
3024
+ {
3025
+ return objectToString(value);
3026
+ }
3027
+ case 'function':
3028
+ case 'object':
3029
+ {
3030
+ if (value === null) {
3031
+ return objectToString(value);
3032
+ }
3033
+ const valueAsRecord = value;
3034
+ const valueToString = lookupGetter(valueAsRecord, 'toString');
3035
+ if (typeof valueToString === 'function') {
3036
+ const stringified = valueToString(valueAsRecord);
3037
+ return typeof stringified === 'string' ? stringified : objectToString(stringified);
3038
+ }
3039
+ return objectToString(value);
3040
+ }
3041
+ default:
3042
+ {
3043
+ return objectToString(value);
3044
+ }
3045
+ }
3046
+ }
2838
3047
  /**
2839
3048
  * This method automatically checks if the prop is function or getter and behaves accordingly.
2840
3049
  *
@@ -2860,9 +3069,17 @@ function lookupGetter(object, prop) {
2860
3069
  }
2861
3070
  return fallbackValue;
2862
3071
  }
3072
+ function isRegex(value) {
3073
+ try {
3074
+ regExpTest(value, '');
3075
+ return true;
3076
+ } catch (_unused) {
3077
+ return false;
3078
+ }
3079
+ }
2863
3080
 
2864
- 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', 'section', 'select', 'shadow', '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']);
2865
- const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
3081
+ 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']);
3082
+ 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']);
2866
3083
  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']);
2867
3084
  // List of SVG elements that are disallowed by default.
2868
3085
  // We still need to know them so that we can do namespace
@@ -2875,40 +3092,31 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
2875
3092
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
2876
3093
  const text = freeze(['#text']);
2877
3094
 
2878
- 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', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', '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', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
2879
- 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', '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']);
2880
- const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', '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']);
3095
+ 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']);
3096
+ 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']);
3097
+ 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']);
2881
3098
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
2882
3099
 
2883
- // eslint-disable-next-line unicorn/better-regex
2884
- const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
2885
- const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
2886
- const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
3100
+ const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
3101
+ const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g);
3102
+ const TMPLIT_EXPR = seal(/\${[\w\W]*/g);
2887
3103
  const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
2888
3104
  const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
2889
- const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
3105
+ 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
2890
3106
  );
2891
3107
  const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
2892
3108
  const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
2893
3109
  );
2894
3110
  const DOCTYPE_NAME = seal(/^html$/i);
2895
3111
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
3112
+ // Markup-significant character probes used by _sanitizeElements.
3113
+ // Shared module-level instances are safe despite the sticky /g flags:
3114
+ // unapply() resets lastIndex for RegExp receivers before every call.
3115
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
3116
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
3117
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
3118
+ const SELF_CLOSING_TAG = seal(/\/>/i);
2896
3119
 
2897
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
2898
- __proto__: null,
2899
- ARIA_ATTR: ARIA_ATTR,
2900
- ATTR_WHITESPACE: ATTR_WHITESPACE,
2901
- CUSTOM_ELEMENT: CUSTOM_ELEMENT,
2902
- DATA_ATTR: DATA_ATTR,
2903
- DOCTYPE_NAME: DOCTYPE_NAME,
2904
- ERB_EXPR: ERB_EXPR,
2905
- IS_ALLOWED_URI: IS_ALLOWED_URI,
2906
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
2907
- MUSTACHE_EXPR: MUSTACHE_EXPR,
2908
- TMPLIT_EXPR: TMPLIT_EXPR
2909
- });
2910
-
2911
- /* eslint-disable @typescript-eslint/indent */
2912
3120
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
2913
3121
  const NODE_TYPE = {
2914
3122
  element: 1,
@@ -2919,7 +3127,7 @@ const NODE_TYPE = {
2919
3127
  // Deprecated
2920
3128
  entityNode: 6,
2921
3129
  // Deprecated
2922
- progressingInstruction: 7,
3130
+ processingInstruction: 7,
2923
3131
  comment: 8,
2924
3132
  document: 9,
2925
3133
  documentType: 10,
@@ -2980,10 +3188,25 @@ const _createHooksMap = function _createHooksMap() {
2980
3188
  uponSanitizeShadowNode: []
2981
3189
  };
2982
3190
  };
3191
+ /**
3192
+ * Resolve a set-valued configuration option: a fresh set built from
3193
+ * cfg[key] when it is an own array property (seeded with a clone of
3194
+ * options.base when given, case-normalized via options.transform),
3195
+ * the fallback set otherwise.
3196
+ *
3197
+ * @param cfg the cloned, prototype-free configuration object
3198
+ * @param key the configuration property to read
3199
+ * @param fallback the set to use when the option is absent or not an array
3200
+ * @param options transform and optional base set to merge into
3201
+ * @returns the resolved set
3202
+ */
3203
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
3204
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
3205
+ };
2983
3206
  function createDOMPurify() {
2984
3207
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
2985
3208
  const DOMPurify = root => createDOMPurify(root);
2986
- DOMPurify.version = '3.2.4';
3209
+ DOMPurify.version = '3.4.11';
2987
3210
  DOMPurify.removed = [];
2988
3211
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
2989
3212
  // Not running in a browser, provide a factory function
@@ -2991,28 +3214,29 @@ function createDOMPurify() {
2991
3214
  DOMPurify.isSupported = false;
2992
3215
  return DOMPurify;
2993
3216
  }
2994
- let {
2995
- document
2996
- } = window;
3217
+ let document = window.document;
2997
3218
  const originalDocument = document;
2998
3219
  const currentScript = originalDocument.currentScript;
2999
- const {
3000
- DocumentFragment,
3001
- HTMLTemplateElement,
3002
- Node,
3003
- Element,
3004
- NodeFilter,
3005
- NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
3006
- HTMLFormElement,
3007
- DOMParser,
3008
- trustedTypes
3009
- } = window;
3220
+ window.DocumentFragment;
3221
+ const HTMLTemplateElement = window.HTMLTemplateElement,
3222
+ Node = window.Node,
3223
+ Element = window.Element,
3224
+ NodeFilter = window.NodeFilter,
3225
+ _window$NamedNodeMap = window.NamedNodeMap;
3226
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
3227
+ window.HTMLFormElement;
3228
+ const DOMParser = window.DOMParser,
3229
+ trustedTypes = window.trustedTypes;
3010
3230
  const ElementPrototype = Element.prototype;
3011
3231
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
3012
3232
  const remove = lookupGetter(ElementPrototype, 'remove');
3013
3233
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
3014
3234
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
3015
3235
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
3236
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
3237
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
3238
+ const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
3239
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
3016
3240
  // As per issue #47, the web-components registry is inherited by a
3017
3241
  // new document created via createHTMLDocument. As per the spec
3018
3242
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -3027,33 +3251,74 @@ function createDOMPurify() {
3027
3251
  }
3028
3252
  let trustedTypesPolicy;
3029
3253
  let emptyHTML = '';
3030
- const {
3031
- implementation,
3032
- createNodeIterator,
3033
- createDocumentFragment,
3034
- getElementsByTagName
3035
- } = document;
3036
- const {
3037
- importNode
3038
- } = originalDocument;
3254
+ // The instance's own internal Trusted Types policy. Unlike a caller-supplied
3255
+ // `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
3256
+ // on duplicate policy names — and is the only policy allowed to persist
3257
+ // across configurations and survive `clearConfig()`.
3258
+ let defaultTrustedTypesPolicy;
3259
+ let defaultTrustedTypesPolicyResolved = false;
3260
+ // Tracks whether we are already inside a call to the configured Trusted Types
3261
+ // policy (`createHTML` or `createScriptURL`). If a supplied policy callback
3262
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
3263
+ // re-enter the policy and recurse until the stack overflows. We detect that
3264
+ // re-entry and throw a clear, actionable error instead. The guard is shared
3265
+ // across both callbacks, because either one re-entering `sanitize` triggers
3266
+ // the same unbounded recursion.
3267
+ let IN_TRUSTED_TYPES_POLICY = 0;
3268
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy() {
3269
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
3270
+ 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.');
3271
+ }
3272
+ };
3273
+ const _createTrustedHTML = function _createTrustedHTML(html) {
3274
+ _assertNotInTrustedTypesPolicy();
3275
+ IN_TRUSTED_TYPES_POLICY++;
3276
+ try {
3277
+ return trustedTypesPolicy.createHTML(html);
3278
+ } finally {
3279
+ IN_TRUSTED_TYPES_POLICY--;
3280
+ }
3281
+ };
3282
+ const _createTrustedScriptURL = function _createTrustedScriptURL(scriptUrl) {
3283
+ _assertNotInTrustedTypesPolicy();
3284
+ IN_TRUSTED_TYPES_POLICY++;
3285
+ try {
3286
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
3287
+ } finally {
3288
+ IN_TRUSTED_TYPES_POLICY--;
3289
+ }
3290
+ };
3291
+ // Lazily resolve (and cache) the instance's internal default policy.
3292
+ // Resolution is attempted at most once: a successful `createPolicy` cannot be
3293
+ // repeated (Trusted Types throws on duplicate names), and a failed or
3294
+ // unsupported attempt must not be retried on every parse.
3295
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy() {
3296
+ if (!defaultTrustedTypesPolicyResolved) {
3297
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
3298
+ defaultTrustedTypesPolicyResolved = true;
3299
+ }
3300
+ return defaultTrustedTypesPolicy;
3301
+ };
3302
+ const _document = document,
3303
+ implementation = _document.implementation,
3304
+ createNodeIterator = _document.createNodeIterator,
3305
+ createDocumentFragment = _document.createDocumentFragment,
3306
+ getElementsByTagName = _document.getElementsByTagName;
3307
+ const importNode = originalDocument.importNode;
3039
3308
  let hooks = _createHooksMap();
3040
3309
  /**
3041
3310
  * Expose whether this browser supports running the full DOMPurify.
3042
3311
  */
3043
3312
  DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
3044
- const {
3045
- MUSTACHE_EXPR,
3046
- ERB_EXPR,
3047
- TMPLIT_EXPR,
3048
- DATA_ATTR,
3049
- ARIA_ATTR,
3050
- IS_SCRIPT_OR_DATA,
3051
- ATTR_WHITESPACE,
3052
- CUSTOM_ELEMENT
3053
- } = EXPRESSIONS;
3054
- let {
3055
- IS_ALLOWED_URI: IS_ALLOWED_URI$1
3056
- } = EXPRESSIONS;
3313
+ const MUSTACHE_EXPR$1 = MUSTACHE_EXPR,
3314
+ ERB_EXPR$1 = ERB_EXPR,
3315
+ TMPLIT_EXPR$1 = TMPLIT_EXPR,
3316
+ DATA_ATTR$1 = DATA_ATTR,
3317
+ ARIA_ATTR$1 = ARIA_ATTR,
3318
+ IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA,
3319
+ ATTR_WHITESPACE$1 = ATTR_WHITESPACE,
3320
+ CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT;
3321
+ let IS_ALLOWED_URI$1 = IS_ALLOWED_URI;
3057
3322
  /**
3058
3323
  * We consider the elements and attributes below to be safe. Ideally
3059
3324
  * don't add any new ones but feel free to remove unwanted ones.
@@ -3094,6 +3359,21 @@ function createDOMPurify() {
3094
3359
  let FORBID_TAGS = null;
3095
3360
  /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
3096
3361
  let FORBID_ATTR = null;
3362
+ /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */
3363
+ const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {
3364
+ tagCheck: {
3365
+ writable: true,
3366
+ configurable: false,
3367
+ enumerable: true,
3368
+ value: null
3369
+ },
3370
+ attributeCheck: {
3371
+ writable: true,
3372
+ configurable: false,
3373
+ enumerable: true,
3374
+ value: null
3375
+ }
3376
+ }));
3097
3377
  /* Decide if ARIA attributes are okay */
3098
3378
  let ALLOW_ARIA_ATTR = true;
3099
3379
  /* Decide if custom data attributes are okay */
@@ -3115,6 +3395,13 @@ function createDOMPurify() {
3115
3395
  let WHOLE_DOCUMENT = false;
3116
3396
  /* Track whether config is already set on this instance of DOMPurify. */
3117
3397
  let SET_CONFIG = false;
3398
+ /* Pristine allowlist bindings captured at setConfig() time. On the
3399
+ * persistent-config path sanitize() restores the sets from these before
3400
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
3401
+ * carry across calls. Null until setConfig() is called; reset by
3402
+ * clearConfig(). */
3403
+ let SET_CONFIG_ALLOWED_TAGS = null;
3404
+ let SET_CONFIG_ALLOWED_ATTR = null;
3118
3405
  /* Decide if all elements (e.g. style, script) must be children of
3119
3406
  * document.body. By default, browsers might move them to document.head */
3120
3407
  let FORCE_BODY = false;
@@ -3157,7 +3444,17 @@ function createDOMPurify() {
3157
3444
  let USE_PROFILES = {};
3158
3445
  /* Tags to ignore content of when KEEP_CONTENT is true */
3159
3446
  let FORBID_CONTENTS = null;
3160
- 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']);
3447
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script',
3448
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
3449
+ // the UA (customizable <select>) — including any on* handlers — and the
3450
+ // engine re-mirrors synchronously whenever a removal changes which
3451
+ // option/selectedcontent is current, even inside DOMPurify's inert
3452
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
3453
+ // mirror target ahead of the walk, which the engine refills, looping
3454
+ // forever (DoS) and amplifying output. Dropping its content on removal
3455
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
3456
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
3457
+ 'selectedcontent', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
3161
3458
  /* Tags that are safe for data: URIs */
3162
3459
  let DATA_URI_TAGS = null;
3163
3460
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
@@ -3173,8 +3470,10 @@ function createDOMPurify() {
3173
3470
  /* Allowed XHTML+XML namespaces */
3174
3471
  let ALLOWED_NAMESPACES = null;
3175
3472
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
3176
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
3177
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
3473
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
3474
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
3475
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
3476
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
3178
3477
  // Certain elements are allowed in both SVG and HTML
3179
3478
  // namespace. We need to specify them explicitly
3180
3479
  // so that they don't get erroneously deleted from
@@ -3216,15 +3515,33 @@ function createDOMPurify() {
3216
3515
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
3217
3516
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
3218
3517
  /* Set configuration parameters */
3219
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
3220
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
3221
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
3222
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
3223
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
3224
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
3225
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};
3226
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};
3227
- USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
3518
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
3519
+ transform: transformCaseFunc
3520
+ });
3521
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
3522
+ transform: transformCaseFunc
3523
+ });
3524
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
3525
+ transform: stringToString
3526
+ });
3527
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
3528
+ transform: transformCaseFunc,
3529
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
3530
+ });
3531
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
3532
+ transform: transformCaseFunc,
3533
+ base: DEFAULT_DATA_URI_TAGS
3534
+ });
3535
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
3536
+ transform: transformCaseFunc
3537
+ });
3538
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
3539
+ transform: transformCaseFunc
3540
+ });
3541
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
3542
+ transform: transformCaseFunc
3543
+ });
3544
+ USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
3228
3545
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
3229
3546
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
3230
3547
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
@@ -3240,20 +3557,22 @@ function createDOMPurify() {
3240
3557
  SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
3241
3558
  KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
3242
3559
  IN_PLACE = cfg.IN_PLACE || false; // Default false
3243
- IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
3244
- NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
3245
- MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
3246
- HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
3247
- CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
3248
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
3249
- CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
3250
- }
3251
- if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
3252
- CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
3253
- }
3254
- if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
3255
- CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
3256
- }
3560
+ IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
3561
+ NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
3562
+ 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
3563
+ 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
3564
+ const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
3565
+ CUSTOM_ELEMENT_HANDLING = create(null);
3566
+ if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
3567
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
3568
+ }
3569
+ if (objectHasOwnProperty(customElementHandling, 'attributeNameCheck') && isRegexOrFunction(customElementHandling.attributeNameCheck)) {
3570
+ CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; // Default undefined
3571
+ }
3572
+ if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
3573
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
3574
+ }
3575
+ seal(CUSTOM_ELEMENT_HANDLING);
3257
3576
  if (SAFE_FOR_TEMPLATES) {
3258
3577
  ALLOW_DATA_ATTR = false;
3259
3578
  }
@@ -3263,7 +3582,7 @@ function createDOMPurify() {
3263
3582
  /* Parse profile info */
3264
3583
  if (USE_PROFILES) {
3265
3584
  ALLOWED_TAGS = addToSet({}, text);
3266
- ALLOWED_ATTR = [];
3585
+ ALLOWED_ATTR = create(null);
3267
3586
  if (USE_PROFILES.html === true) {
3268
3587
  addToSet(ALLOWED_TAGS, html$1);
3269
3588
  addToSet(ALLOWED_ATTR, html);
@@ -3284,28 +3603,46 @@ function createDOMPurify() {
3284
3603
  addToSet(ALLOWED_ATTR, xml);
3285
3604
  }
3286
3605
  }
3606
+ /* Always reset function-based ADD_TAGS / ADD_ATTR checks to prevent
3607
+ * leaking across calls when switching from function to array config */
3608
+ EXTRA_ELEMENT_HANDLING.tagCheck = null;
3609
+ EXTRA_ELEMENT_HANDLING.attributeCheck = null;
3287
3610
  /* Merge configuration parameters */
3288
- if (cfg.ADD_TAGS) {
3289
- if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
3290
- ALLOWED_TAGS = clone(ALLOWED_TAGS);
3611
+ if (objectHasOwnProperty(cfg, 'ADD_TAGS')) {
3612
+ if (typeof cfg.ADD_TAGS === 'function') {
3613
+ EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;
3614
+ } else if (arrayIsArray(cfg.ADD_TAGS)) {
3615
+ if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
3616
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
3617
+ }
3618
+ addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
3291
3619
  }
3292
- addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
3293
3620
  }
3294
- if (cfg.ADD_ATTR) {
3295
- if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
3296
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
3621
+ if (objectHasOwnProperty(cfg, 'ADD_ATTR')) {
3622
+ if (typeof cfg.ADD_ATTR === 'function') {
3623
+ EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;
3624
+ } else if (arrayIsArray(cfg.ADD_ATTR)) {
3625
+ if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
3626
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
3627
+ }
3628
+ addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
3297
3629
  }
3298
- addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
3299
3630
  }
3300
- if (cfg.ADD_URI_SAFE_ATTR) {
3631
+ if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
3301
3632
  addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
3302
3633
  }
3303
- if (cfg.FORBID_CONTENTS) {
3634
+ if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
3304
3635
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
3305
3636
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
3306
3637
  }
3307
3638
  addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
3308
3639
  }
3640
+ if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
3641
+ if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
3642
+ FORBID_CONTENTS = clone(FORBID_CONTENTS);
3643
+ }
3644
+ addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);
3645
+ }
3309
3646
  /* Add #text in case KEEP_CONTENT is set to true */
3310
3647
  if (KEEP_CONTENT) {
3311
3648
  ALLOWED_TAGS['#text'] = true;
@@ -3319,6 +3656,13 @@ function createDOMPurify() {
3319
3656
  addToSet(ALLOWED_TAGS, ['tbody']);
3320
3657
  delete FORBID_TAGS.tbody;
3321
3658
  }
3659
+ // Re-derive the active Trusted Types policy from this configuration on
3660
+ // every parse. The active policy must never be sticky closure state that
3661
+ // outlives the config that set it: a caller-supplied policy left in place
3662
+ // after `clearConfig()` — or after a later call that supplied none, or
3663
+ // `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
3664
+ // `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
3665
+ // See GHSA-vxr8-fq34-vvx9.
3322
3666
  if (cfg.TRUSTED_TYPES_POLICY) {
3323
3667
  if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
3324
3668
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
@@ -3326,18 +3670,45 @@ function createDOMPurify() {
3326
3670
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
3327
3671
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
3328
3672
  }
3329
- // Overwrite existing TrustedTypes policy.
3673
+ // A caller-supplied policy applies to this configuration only.
3674
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
3330
3675
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
3331
- // Sign local variables required by `sanitize`.
3332
- emptyHTML = trustedTypesPolicy.createHTML('');
3676
+ // Sign local variables required by `sanitize`. If the supplied policy's
3677
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
3678
+ // throws via the re-entrancy guard. Restore the previous policy first so
3679
+ // the instance is not left in a poisoned state. See #1422.
3680
+ try {
3681
+ emptyHTML = _createTrustedHTML('');
3682
+ } catch (error) {
3683
+ trustedTypesPolicy = previousTrustedTypesPolicy;
3684
+ throw error;
3685
+ }
3686
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
3687
+ // Explicit opt-out for this call: perform no Trusted Types signing and
3688
+ // create nothing (so a strict `trusted-types` CSP that disallows a
3689
+ // `dompurify` policy can still call `sanitize` from inside its own
3690
+ // policy — see #1422). Resetting to `undefined` rather than a sticky
3691
+ // `null` also drops any previously retained caller policy, so it cannot
3692
+ // resurface on a later call, while still allowing the next config-less
3693
+ // call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
3694
+ trustedTypesPolicy = undefined;
3695
+ emptyHTML = '';
3333
3696
  } else {
3334
- // Uninitialized policy, attempt to initialize the internal dompurify policy.
3697
+ // No policy supplied: keep the currently active policy if one is set — a
3698
+ // previously supplied policy is intentionally sticky across config-less
3699
+ // calls — otherwise fall back to the instance's own internal policy,
3700
+ // created at most once. (A policy supplied for a *single* call still
3701
+ // lingers by design; what must not linger is a policy whose configuration
3702
+ // has been torn down via `clearConfig()`, which restores the default.)
3335
3703
  if (trustedTypesPolicy === undefined) {
3336
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
3704
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
3337
3705
  }
3338
- // If creating the internal policy succeeded sign internal variables.
3339
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
3340
- emptyHTML = trustedTypesPolicy.createHTML('');
3706
+ // Sign internal variables only when a policy is active. A falsy policy
3707
+ // (Trusted Types unsupported, creation failed, or an explicit opt-out)
3708
+ // leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
3709
+ // a non-policy and throw. See #1422.
3710
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
3711
+ emptyHTML = _createTrustedHTML('');
3341
3712
  }
3342
3713
  }
3343
3714
  // Prevent further manipulation of configuration.
@@ -3352,6 +3723,77 @@ function createDOMPurify() {
3352
3723
  * correctly. */
3353
3724
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
3354
3725
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
3726
+ /**
3727
+ * Namespace rules for an element in the SVG namespace.
3728
+ *
3729
+ * @param tagName the element's lowercase tag name
3730
+ * @param parent the (possibly simulated) parent node
3731
+ * @param parentTagName the parent's lowercase tag name
3732
+ * @returns true if a spec-compliant parser could produce this element
3733
+ */
3734
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
3735
+ // The only way to switch from HTML namespace to SVG
3736
+ // is via <svg>. If it happens via any other tag, then
3737
+ // it should be killed.
3738
+ if (parent.namespaceURI === HTML_NAMESPACE) {
3739
+ return tagName === 'svg';
3740
+ }
3741
+ // The only way to switch from MathML to SVG is via <svg>
3742
+ // if the parent is either <annotation-xml> or a MathML
3743
+ // text integration point.
3744
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
3745
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
3746
+ }
3747
+ // We only allow elements that are defined in SVG
3748
+ // spec. All others are disallowed in SVG namespace.
3749
+ return Boolean(ALL_SVG_TAGS[tagName]);
3750
+ };
3751
+ /**
3752
+ * Namespace rules for an element in the MathML namespace.
3753
+ *
3754
+ * @param tagName the element's lowercase tag name
3755
+ * @param parent the (possibly simulated) parent node
3756
+ * @param parentTagName the parent's lowercase tag name
3757
+ * @returns true if a spec-compliant parser could produce this element
3758
+ */
3759
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
3760
+ // The only way to switch from HTML namespace to MathML
3761
+ // is via <math>. If it happens via any other tag, then
3762
+ // it should be killed.
3763
+ if (parent.namespaceURI === HTML_NAMESPACE) {
3764
+ return tagName === 'math';
3765
+ }
3766
+ // The only way to switch from SVG to MathML is via
3767
+ // <math> and HTML integration points
3768
+ if (parent.namespaceURI === SVG_NAMESPACE) {
3769
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
3770
+ }
3771
+ // We only allow elements that are defined in MathML
3772
+ // spec. All others are disallowed in MathML namespace.
3773
+ return Boolean(ALL_MATHML_TAGS[tagName]);
3774
+ };
3775
+ /**
3776
+ * Namespace rules for an element in the HTML namespace.
3777
+ *
3778
+ * @param tagName the element's lowercase tag name
3779
+ * @param parent the (possibly simulated) parent node
3780
+ * @param parentTagName the parent's lowercase tag name
3781
+ * @returns true if a spec-compliant parser could produce this element
3782
+ */
3783
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
3784
+ // The only way to switch from SVG to HTML is via
3785
+ // HTML integration points, and from MathML to HTML
3786
+ // is via MathML text integration points
3787
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
3788
+ return false;
3789
+ }
3790
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
3791
+ return false;
3792
+ }
3793
+ // We disallow tags that are specific for MathML
3794
+ // or SVG and should never appear in HTML namespace
3795
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
3796
+ };
3355
3797
  /**
3356
3798
  * @param element a DOM element whose namespace is being checked
3357
3799
  * @returns Return false if the element has a
@@ -3374,51 +3816,13 @@ function createDOMPurify() {
3374
3816
  return false;
3375
3817
  }
3376
3818
  if (element.namespaceURI === SVG_NAMESPACE) {
3377
- // The only way to switch from HTML namespace to SVG
3378
- // is via <svg>. If it happens via any other tag, then
3379
- // it should be killed.
3380
- if (parent.namespaceURI === HTML_NAMESPACE) {
3381
- return tagName === 'svg';
3382
- }
3383
- // The only way to switch from MathML to SVG is via`
3384
- // svg if parent is either <annotation-xml> or MathML
3385
- // text integration points.
3386
- if (parent.namespaceURI === MATHML_NAMESPACE) {
3387
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
3388
- }
3389
- // We only allow elements that are defined in SVG
3390
- // spec. All others are disallowed in SVG namespace.
3391
- return Boolean(ALL_SVG_TAGS[tagName]);
3819
+ return _checkSvgNamespace(tagName, parent, parentTagName);
3392
3820
  }
3393
3821
  if (element.namespaceURI === MATHML_NAMESPACE) {
3394
- // The only way to switch from HTML namespace to MathML
3395
- // is via <math>. If it happens via any other tag, then
3396
- // it should be killed.
3397
- if (parent.namespaceURI === HTML_NAMESPACE) {
3398
- return tagName === 'math';
3399
- }
3400
- // The only way to switch from SVG to MathML is via
3401
- // <math> and HTML integration points
3402
- if (parent.namespaceURI === SVG_NAMESPACE) {
3403
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
3404
- }
3405
- // We only allow elements that are defined in MathML
3406
- // spec. All others are disallowed in MathML namespace.
3407
- return Boolean(ALL_MATHML_TAGS[tagName]);
3822
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
3408
3823
  }
3409
3824
  if (element.namespaceURI === HTML_NAMESPACE) {
3410
- // The only way to switch from SVG to HTML is via
3411
- // HTML integration points, and from MathML to HTML
3412
- // is via MathML text integration points
3413
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
3414
- return false;
3415
- }
3416
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
3417
- return false;
3418
- }
3419
- // We disallow tags that are specific for MathML
3420
- // or SVG and should never appear in HTML namespace
3421
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
3825
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
3422
3826
  }
3423
3827
  // For XHTML and XML documents that support custom namespaces
3424
3828
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -3443,7 +3847,74 @@ function createDOMPurify() {
3443
3847
  // eslint-disable-next-line unicorn/prefer-dom-node-remove
3444
3848
  getParentNode(node).removeChild(node);
3445
3849
  } catch (_) {
3850
+ /* The normal detach failed — this is reached for a parentless node
3851
+ (getParentNode() is null, so .removeChild throws). Element.prototype
3852
+ .remove() is itself a spec no-op on a parentless node, so a recorded
3853
+ "removal" would otherwise hand the caller back an intact,
3854
+ payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
3855
+ the style-with-element-child rule decided to kill). Fail closed by
3856
+ throwing — exactly as a clobbered root does at the IN_PLACE entry —
3857
+ rather than trying to "neutralize" the node via its own methods.
3858
+ Neutralizing would mean calling getAttributeNames()/removeAttribute()
3859
+ on the node, both of which a <form> root can clobber via a named child
3860
+ (and _isClobbered does not even probe getAttributeNames), so the
3861
+ neutralize step could itself be silently defeated, leaving the payload
3862
+ intact. A throw touches only the cached, clobber-safe remove() and
3863
+ getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
3864
+ to every root-kill reason. REPORT-3.
3865
+ This lives inside the catch, so it never fires for a normally-removed
3866
+ in-tree node: those have a parent, removeChild() succeeds, and the
3867
+ catch is not entered. Only a kept (parentless) root reaches here. */
3446
3868
  remove(node);
3869
+ if (!getParentNode(node)) {
3870
+ throw typeErrorCreate('a node selected for removal could not be detached from its tree ' + 'and cannot be safely returned; refusing to sanitize in place');
3871
+ }
3872
+ }
3873
+ };
3874
+ /**
3875
+ * _neutralizeRoot
3876
+ *
3877
+ * Fail-closed teardown of an in-place root after the sanitize walk aborts
3878
+ * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
3879
+ * custom element's reaction detaches a node so `_forceRemove`'s deliberate
3880
+ * parentless guard throws, or any other re-entrant engine mutation — would
3881
+ * otherwise leave the caller's *live* tree half-sanitized, with everything
3882
+ * after the abort point still carrying its handlers. There is no safe way
3883
+ * to resume the walk (the tree mutated under us), so we strip the root bare:
3884
+ * remove every child and every attribute, then let the caller's catch see
3885
+ * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
3886
+ * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
3887
+ *
3888
+ * @param root the in-place root to empty
3889
+ */
3890
+ const _neutralizeRoot = function _neutralizeRoot(root) {
3891
+ const childNodes = getChildNodes(root);
3892
+ if (childNodes) {
3893
+ const snapshot = [];
3894
+ arrayForEach(childNodes, child => {
3895
+ arrayPush(snapshot, child);
3896
+ });
3897
+ arrayForEach(snapshot, child => {
3898
+ try {
3899
+ remove(child);
3900
+ } catch (_) {
3901
+ /* Best-effort teardown; a still-attached child is handled below */
3902
+ }
3903
+ });
3904
+ }
3905
+ const attributes = getAttributes(root);
3906
+ if (attributes) {
3907
+ for (let i = attributes.length - 1; i >= 0; --i) {
3908
+ const attribute = attributes[i];
3909
+ const name = attribute && attribute.name;
3910
+ if (typeof name === 'string') {
3911
+ try {
3912
+ root.removeAttribute(name);
3913
+ } catch (_) {
3914
+ /* Clobbered removeAttribute — ignore (fail-closed best effort) */
3915
+ }
3916
+ }
3917
+ }
3447
3918
  }
3448
3919
  };
3449
3920
  /**
@@ -3478,6 +3949,72 @@ function createDOMPurify() {
3478
3949
  }
3479
3950
  }
3480
3951
  };
3952
+ /**
3953
+ * _stripDisallowedAttributes
3954
+ *
3955
+ * Removes every attribute the active configuration does not allow from a
3956
+ * single element, using the same allowlist as the main attribute pass (so
3957
+ * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
3958
+ * neutralise nodes that are being discarded from an in-place tree.
3959
+ *
3960
+ * @param element the element to strip
3961
+ */
3962
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
3963
+ const attributes = getAttributes(element);
3964
+ if (!attributes) {
3965
+ return;
3966
+ }
3967
+ for (let i = attributes.length - 1; i >= 0; --i) {
3968
+ const attribute = attributes[i];
3969
+ const name = attribute && attribute.name;
3970
+ if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
3971
+ continue;
3972
+ }
3973
+ try {
3974
+ element.removeAttribute(name);
3975
+ } catch (_) {
3976
+ /* Clobbered removeAttribute on a doomed node — ignore */
3977
+ }
3978
+ }
3979
+ };
3980
+ /**
3981
+ * _neutralizeSubtree
3982
+ *
3983
+ * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
3984
+ * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
3985
+ * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
3986
+ * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
3987
+ * those dropped nodes are detached from the caller's LIVE tree but a
3988
+ * handler-bearing original among them (an `<img onerror>`/`<video>` that was
3989
+ * loading) keeps its queued resource event, which fires in page scope after
3990
+ * sanitize returns. This walks a removed subtree and strips every attribute
3991
+ * the active configuration does not allow — so `on*` handlers are cancelled
3992
+ * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
3993
+ * blocklist. Run synchronously before sanitize returns, i.e. before any
3994
+ * queued event can fire. Hook-free by design: these nodes leave the output,
3995
+ * so firing attribute hooks for them would be surprising. Clobber-safe reads;
3996
+ * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
3997
+ * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
3998
+ * the `<img>`, are reached and scrubbed).
3999
+ *
4000
+ * @param root the root of a removed subtree to neutralise
4001
+ */
4002
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
4003
+ const stack = [root];
4004
+ while (stack.length > 0) {
4005
+ const node = stack.pop();
4006
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
4007
+ if (nodeType === NODE_TYPE.element) {
4008
+ _stripDisallowedAttributes(node);
4009
+ }
4010
+ const childNodes = getChildNodes(node);
4011
+ if (childNodes) {
4012
+ for (let i = childNodes.length - 1; i >= 0; --i) {
4013
+ stack.push(childNodes[i]);
4014
+ }
4015
+ }
4016
+ }
4017
+ };
3481
4018
  /**
3482
4019
  * _initDocument
3483
4020
  *
@@ -3499,7 +4036,7 @@ function createDOMPurify() {
3499
4036
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
3500
4037
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
3501
4038
  }
3502
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
4039
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
3503
4040
  /*
3504
4041
  * Use the DOMParser API by default, fallback later if needs be
3505
4042
  * DOMParser not work for svg when has multiple root element.
@@ -3539,29 +4076,254 @@ function createDOMPurify() {
3539
4076
  // eslint-disable-next-line no-bitwise
3540
4077
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
3541
4078
  };
4079
+ /**
4080
+ * Replace template expression syntax (mustache, ERB, template
4081
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
4082
+ * sites. Order matters: mustache, then ERB, then template literal.
4083
+ *
4084
+ * @param value the string to scrub
4085
+ * @returns the scrubbed string
4086
+ */
4087
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
4088
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
4089
+ value = stringReplace(value, ERB_EXPR$1, ' ');
4090
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
4091
+ return value;
4092
+ };
4093
+ /**
4094
+ * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
4095
+ * character data of an element subtree. Used as the final safety net for
4096
+ * SAFE_FOR_TEMPLATES on every DOM-returning code path so that expressions
4097
+ * which only form after text-node normalization (e.g. fragments split across
4098
+ * stripped elements) cannot survive into a template-evaluating framework.
4099
+ *
4100
+ * Walks text/comment/CDATA/processing-instruction nodes and mutates `.data`
4101
+ * in place rather than round-tripping through innerHTML. This preserves
4102
+ * descendant node references (important for IN_PLACE callers), avoids a
4103
+ * serialize/reparse cycle, and reads literal character data — which means
4104
+ * `<%...%>` in text content matches the ERB regex against its real bytes
4105
+ * instead of the HTML-entity-escaped form innerHTML would produce.
4106
+ *
4107
+ * Attribute values are not visited here; SAFE_FOR_TEMPLATES handling for
4108
+ * attributes is performed during the per-node `_sanitizeAttributes` pass.
4109
+ *
4110
+ * @param node The root element whose character data should be scrubbed.
4111
+ */
4112
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
4113
+ var _node$querySelectorAl;
4114
+ node.normalize();
4115
+ const walker = createNodeIterator.call(node.ownerDocument || node, node,
4116
+ // eslint-disable-next-line no-bitwise
4117
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
4118
+ let currentNode = walker.nextNode();
4119
+ while (currentNode) {
4120
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
4121
+ currentNode = walker.nextNode();
4122
+ }
4123
+ // NodeIterator does not descend into <template>.content per the DOM spec,
4124
+ // so we must explicitly recurse into each template's content fragment,
4125
+ // mirroring the approach used by _sanitizeShadowDOM.
4126
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
4127
+ if (templates) {
4128
+ arrayForEach(templates, tmpl => {
4129
+ if (_isDocumentFragment(tmpl.content)) {
4130
+ _scrubTemplateExpressions2(tmpl.content);
4131
+ }
4132
+ });
4133
+ }
4134
+ };
3542
4135
  /**
3543
4136
  * _isClobbered
3544
4137
  *
4138
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
4139
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
4140
+ * `name` attribute matching a prototype property shadows that property
4141
+ * on direct reads. We use this check at the IN_PLACE entry-point and
4142
+ * during attribute sanitization to refuse clobbered forms.
4143
+ *
3545
4144
  * @param element element to check for clobbering attacks
3546
4145
  * @return true if clobbered, false if safe
3547
4146
  */
3548
4147
  const _isClobbered = function _isClobbered(element) {
3549
- 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');
4148
+ // Realm-independent tag-name probe. If we can't determine the tag
4149
+ // name at all, we can't reason about clobbering — return false
4150
+ // (the caller's other defences still apply).
4151
+ const realTagName = getNodeName ? getNodeName(element) : null;
4152
+ if (typeof realTagName !== 'string') {
4153
+ return false;
4154
+ }
4155
+ if (transformCaseFunc(realTagName) !== 'form') {
4156
+ return false;
4157
+ }
4158
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
4159
+ // Realm-safe NamedNodeMap detection: equality against the cached
4160
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
4161
+ // makes the direct read diverge from the cached read; a clean form
4162
+ // (same-realm OR foreign-realm) has both reads pointing at the same
4163
+ // canonical NamedNodeMap.
4164
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
4165
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
4166
+ // returns the integer 1 for any Element regardless of realm; direct
4167
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
4168
+ // the named child element. Cheap addition — nodeType is read from
4169
+ // an internal slot, no serialization cost — and removes a residual
4170
+ // clobbering surface used by several mXSS / PI / comment branches
4171
+ // in _sanitizeElements that compare currentNode.nodeType directly.
4172
+ element.nodeType !== getNodeType(element) ||
4173
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
4174
+ // "childNodes" shadows the prototype getter. Direct reads of
4175
+ // form.childNodes from a clobbered form return the named child
4176
+ // instead of the real NodeList, so any walk that reads it directly
4177
+ // skips the form's real children. Compare the direct read to the
4178
+ // cached Node.prototype getter — when the form's named-property
4179
+ // getter intercepts the read, the two values differ and we flag
4180
+ // the form. This catches every clobbering child type (input,
4181
+ // select, etc.) regardless of whether the named child happens to
4182
+ // carry a numeric .length, which a typeof-based probe would miss
4183
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
4184
+ element.childNodes !== getChildNodes(element);
4185
+ };
4186
+ /**
4187
+ * Checks whether the given value is a DocumentFragment from any realm.
4188
+ *
4189
+ * The realm-independent replacement reads `nodeType` through the cached
4190
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
4191
+ * constant (11). nodeType is a numeric value resolved from the node's
4192
+ * internal slot, identical across realms for the same kind of node.
4193
+ *
4194
+ * @param value object to check
4195
+ * @return true if value is a DocumentFragment-shaped node from any realm
4196
+ */
4197
+ const _isDocumentFragment = function _isDocumentFragment(value) {
4198
+ if (!getNodeType || typeof value !== 'object' || value === null) {
4199
+ return false;
4200
+ }
4201
+ try {
4202
+ return getNodeType(value) === NODE_TYPE.documentFragment;
4203
+ } catch (_) {
4204
+ return false;
4205
+ }
3550
4206
  };
3551
4207
  /**
3552
- * Checks whether the given object is a DOM node.
4208
+ * Checks whether the given object is a DOM node, including nodes that
4209
+ * originate from a different window/realm (e.g. an iframe's
4210
+ * contentDocument). The previous `value instanceof Node` check was
4211
+ * realm-bound: nodes from a different window failed it, causing
4212
+ * sanitize() to silently stringify them and reset IN_PLACE to false,
4213
+ * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
3553
4214
  *
3554
4215
  * @param value object to check whether it's a DOM node
3555
- * @return true is object is a DOM node
4216
+ * @return true if value is a DOM node from any realm
3556
4217
  */
3557
4218
  const _isNode = function _isNode(value) {
3558
- return typeof Node === 'function' && value instanceof Node;
4219
+ if (!getNodeType || typeof value !== 'object' || value === null) {
4220
+ return false;
4221
+ }
4222
+ try {
4223
+ return typeof getNodeType(value) === 'number';
4224
+ } catch (_) {
4225
+ return false;
4226
+ }
3559
4227
  };
3560
4228
  function _executeHooks(hooks, currentNode, data) {
4229
+ if (hooks.length === 0) {
4230
+ return;
4231
+ }
3561
4232
  arrayForEach(hooks, hook => {
3562
4233
  hook.call(DOMPurify, currentNode, data, CONFIG);
3563
4234
  });
3564
4235
  }
4236
+ /**
4237
+ * Structural-threat checks that condemn a node regardless of the
4238
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
4239
+ * processing instructions, markup-bearing comments. Pure predicate;
4240
+ * the caller removes. Check order is load-bearing.
4241
+ *
4242
+ * @param currentNode the node to inspect
4243
+ * @param tagName the node's transformCaseFunc'd tag name
4244
+ * @return true if the node must be removed
4245
+ */
4246
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
4247
+ /* Detect mXSS attempts abusing namespace confusion */
4248
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
4249
+ return true;
4250
+ }
4251
+ /* Remove risky CSS construction leading to mXSS */
4252
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
4253
+ return true;
4254
+ }
4255
+ /* Remove any occurrence of processing instructions */
4256
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
4257
+ return true;
4258
+ }
4259
+ /* Remove any kind of possibly harmful comments */
4260
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
4261
+ return true;
4262
+ }
4263
+ return false;
4264
+ };
4265
+ /**
4266
+ * Handle a node whose tag is forbidden or not allowlisted: keep
4267
+ * allowed custom elements (false return exits _sanitizeElements
4268
+ * early - namespace/fallback checks and the afterSanitizeElements
4269
+ * hook are intentionally skipped for kept custom elements), else
4270
+ * hoist content per KEEP_CONTENT and remove.
4271
+ *
4272
+ * @param currentNode the disallowed node
4273
+ * @param tagName the node's transformCaseFunc'd tag name
4274
+ * @return true if the node was removed, false if kept
4275
+ */
4276
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
4277
+ /* Check if we have a custom element to handle */
4278
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
4279
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
4280
+ return false;
4281
+ }
4282
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
4283
+ return false;
4284
+ }
4285
+ }
4286
+ /* Keep content except for bad-listed elements.
4287
+ Use the cached prototype getters exclusively — the previous code
4288
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
4289
+ fallbacks, but the cached getters always return the canonical
4290
+ value (or null for a real parent-less node), so the fallback
4291
+ path was dead in safe cases and a clobbering surface in unsafe
4292
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
4293
+ parentNode)` check already gates correctly. */
4294
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
4295
+ const parentNode = getParentNode(currentNode);
4296
+ const childNodes = getChildNodes(currentNode);
4297
+ if (childNodes && parentNode) {
4298
+ const childCount = childNodes.length;
4299
+ /* In-place: hoist the *original* children so the iterator visits
4300
+ and sanitises them through the same allowlist pass as every other
4301
+ node. The caller built the tree in the live document, so the
4302
+ originals carry already-queued resource events (`<img onerror>`,
4303
+ `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
4304
+ those originals detached but still armed, firing in page scope
4305
+ while the returned tree looked clean. Moving is safe in-place: the
4306
+ root is pre-validated as an allowed tag and so is never the node
4307
+ being removed, which keeps `parentNode` inside the iterator root
4308
+ and the relocated child inside the serialised tree.
4309
+ Otherwise (string / DOM-copy paths): clone. The iterator is rooted
4310
+ at — and the result serialised from — `body`, so a restrictive
4311
+ ALLOWED_TAGS that removes `body` itself must leave its content in
4312
+ place, which only cloning does; and those paths parse into an
4313
+ inert document, so their discarded originals never had a queued
4314
+ event to neutralise.
4315
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
4316
+ valid whether we move (drops the trailing entry) or clone (leaves
4317
+ the list intact). */
4318
+ for (let i = childCount - 1; i >= 0; --i) {
4319
+ const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
4320
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
4321
+ }
4322
+ }
4323
+ }
4324
+ _forceRemove(currentNode);
4325
+ return true;
4326
+ };
3565
4327
  /**
3566
4328
  * _sanitizeElements
3567
4329
  *
@@ -3572,7 +4334,6 @@ function createDOMPurify() {
3572
4334
  * @return true if node was killed, false if left alive
3573
4335
  */
3574
4336
  const _sanitizeElements = function _sanitizeElements(currentNode) {
3575
- let content = null;
3576
4337
  /* Execute a hook if present */
3577
4338
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
3578
4339
  /* Check if element is clobbered or can clobber */
@@ -3581,71 +4342,41 @@ function createDOMPurify() {
3581
4342
  return true;
3582
4343
  }
3583
4344
  /* Now let's check the element's type and name */
3584
- const tagName = transformCaseFunc(currentNode.nodeName);
4345
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
3585
4346
  /* Execute a hook if present */
3586
4347
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
3587
4348
  tagName,
3588
4349
  allowedTags: ALLOWED_TAGS
3589
4350
  });
3590
- /* Detect mXSS attempts abusing namespace confusion */
3591
- if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
3592
- _forceRemove(currentNode);
3593
- return true;
3594
- }
3595
- /* Remove any occurrence of processing instructions */
3596
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
3597
- _forceRemove(currentNode);
3598
- return true;
3599
- }
3600
- /* Remove any kind of possibly harmful comments */
3601
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
4351
+ /* Remove mXSS vectors, processing instructions and risky comments */
4352
+ if (_isUnsafeNode(currentNode, tagName)) {
3602
4353
  _forceRemove(currentNode);
3603
4354
  return true;
3604
4355
  }
3605
4356
  /* Remove element if anything forbids its presence */
3606
- if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
3607
- /* Check if we have a custom element to handle */
3608
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
3609
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
3610
- return false;
3611
- }
3612
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
3613
- return false;
3614
- }
3615
- }
3616
- /* Keep content except for bad-listed elements */
3617
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
3618
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
3619
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
3620
- if (childNodes && parentNode) {
3621
- const childCount = childNodes.length;
3622
- for (let i = childCount - 1; i >= 0; --i) {
3623
- const childClone = cloneNode(childNodes[i], true);
3624
- childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
3625
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
3626
- }
3627
- }
3628
- }
3629
- _forceRemove(currentNode);
3630
- return true;
3631
- }
3632
- /* Check whether element has a valid namespace */
3633
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
4357
+ if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
4358
+ return _sanitizeDisallowedNode(currentNode, tagName);
4359
+ }
4360
+ /* Check whether element has a valid namespace.
4361
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
4362
+ nodeType getter rather than `instanceof Element`, which is realm-
4363
+ bound and short-circuits to false for any node minted in a different
4364
+ realm — letting a foreign-realm element with a forbidden namespace
4365
+ slip past the namespace check entirely. */
4366
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
4367
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
3634
4368
  _forceRemove(currentNode);
3635
4369
  return true;
3636
4370
  }
3637
4371
  /* Make sure that older browsers don't get fallback-tag mXSS */
3638
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
4372
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
3639
4373
  _forceRemove(currentNode);
3640
4374
  return true;
3641
4375
  }
3642
4376
  /* Sanitize element content to be template-safe */
3643
4377
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
3644
4378
  /* Get the element's text content */
3645
- content = currentNode.textContent;
3646
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
3647
- content = stringReplace(content, expr, ' ');
3648
- });
4379
+ const content = _stripTemplateExpressions(currentNode.textContent);
3649
4380
  if (currentNode.textContent !== content) {
3650
4381
  arrayPush(DOMPurify.removed, {
3651
4382
  element: currentNode.cloneNode()
@@ -3667,31 +4398,40 @@ function createDOMPurify() {
3667
4398
  */
3668
4399
  // eslint-disable-next-line complexity
3669
4400
  const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
4401
+ /* FORBID_ATTR must always win, even if ADD_ATTR predicate would allow it */
4402
+ if (FORBID_ATTR[lcName]) {
4403
+ return false;
4404
+ }
3670
4405
  /* Make sure attribute cannot clobber */
3671
4406
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
3672
4407
  return false;
3673
4408
  }
4409
+ const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
3674
4410
  /* Allow valid data-* attributes: At least one character after "-"
3675
4411
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
3676
4412
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
3677
4413
  We don't need to check the value; it's always URI safe. */
3678
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
4414
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
3679
4415
  if (
3680
4416
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
3681
4417
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
3682
4418
  // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
3683
- _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||
4419
+ _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) ||
3684
4420
  // Alternative, second condition checks if it's an `is`-attribute, AND
3685
4421
  // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
3686
4422
  lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
3687
4423
  return false;
3688
4424
  }
3689
4425
  /* Check value is safe. First, is attr inert? If so, is safe */
3690
- } 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) {
4426
+ } 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) {
3691
4427
  return false;
3692
4428
  } else ;
3693
4429
  return true;
3694
4430
  };
4431
+ /* Names the HTML spec reserves from valid-custom-element-name; these must
4432
+ * never be treated as basic custom elements even when a permissive
4433
+ * CUSTOM_ELEMENT_HANDLING.tagNameCheck is configured. */
4434
+ const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ['annotation-xml', 'color-profile', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'missing-glyph']);
3695
4435
  /**
3696
4436
  * _isBasicCustomElement
3697
4437
  * checks if at least one dash is included in tagName, and it's not the first char
@@ -3701,7 +4441,64 @@ function createDOMPurify() {
3701
4441
  * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
3702
4442
  */
3703
4443
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
3704
- return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
4444
+ return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
4445
+ };
4446
+ /**
4447
+ * Wrap an attribute value in the matching Trusted Types object when
4448
+ * the active policy requires it. Namespaced attributes pass through
4449
+ * unchanged (no TT support yet, see
4450
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
4451
+ *
4452
+ * @param lcTag lowercase tag name of the containing element
4453
+ * @param lcName lowercase attribute name
4454
+ * @param namespaceURI the attribute's namespace, if any
4455
+ * @param value the attribute value to wrap
4456
+ * @return the value, wrapped when Trusted Types demand it
4457
+ */
4458
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
4459
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
4460
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
4461
+ case 'TrustedHTML':
4462
+ {
4463
+ return _createTrustedHTML(value);
4464
+ }
4465
+ case 'TrustedScriptURL':
4466
+ {
4467
+ return _createTrustedScriptURL(value);
4468
+ }
4469
+ }
4470
+ }
4471
+ return value;
4472
+ };
4473
+ /**
4474
+ * Write a modified attribute value back onto the element. On
4475
+ * success, re-probe for clobbering introduced by the new value and
4476
+ * remove the element when found; otherwise pop the removal entry
4477
+ * recorded by the earlier _removeAttribute (long-standing pairing
4478
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
4479
+ * failure, remove the attribute instead.
4480
+ *
4481
+ * @param currentNode the element carrying the attribute
4482
+ * @param name the attribute name as present on the element
4483
+ * @param namespaceURI the attribute's namespace, if any
4484
+ * @param value the new attribute value
4485
+ */
4486
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
4487
+ try {
4488
+ if (namespaceURI) {
4489
+ currentNode.setAttributeNS(namespaceURI, name, value);
4490
+ } else {
4491
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
4492
+ currentNode.setAttribute(name, value);
4493
+ }
4494
+ if (_isClobbered(currentNode)) {
4495
+ _forceRemove(currentNode);
4496
+ } else {
4497
+ arrayPop(DOMPurify.removed);
4498
+ }
4499
+ } catch (_) {
4500
+ _removeAttribute(name, currentNode);
4501
+ }
3705
4502
  };
3706
4503
  /**
3707
4504
  * _sanitizeAttributes
@@ -3716,9 +4513,7 @@ function createDOMPurify() {
3716
4513
  const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
3717
4514
  /* Execute a hook if present */
3718
4515
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
3719
- const {
3720
- attributes
3721
- } = currentNode;
4516
+ const attributes = currentNode.attributes;
3722
4517
  /* Check if we have attributes; if not we might have a text node */
3723
4518
  if (!attributes || _isClobbered(currentNode)) {
3724
4519
  return;
@@ -3731,16 +4526,16 @@ function createDOMPurify() {
3731
4526
  forceKeepAttr: undefined
3732
4527
  };
3733
4528
  let l = attributes.length;
4529
+ const lcTag = transformCaseFunc(currentNode.nodeName);
3734
4530
  /* Go backwards over all attributes; safely remove bad ones */
3735
4531
  while (l--) {
3736
4532
  const attr = attributes[l];
3737
- const {
3738
- name,
3739
- namespaceURI,
3740
- value: attrValue
3741
- } = attr;
4533
+ const name = attr.name,
4534
+ namespaceURI = attr.namespaceURI,
4535
+ attrValue = attr.value;
3742
4536
  const lcName = transformCaseFunc(name);
3743
- let value = name === 'value' ? attrValue : stringTrim(attrValue);
4537
+ const initValue = attrValue;
4538
+ let value = name === 'value' ? initValue : stringTrim(initValue);
3744
4539
  /* Execute a hook if present */
3745
4540
  hookEvent.attrName = lcName;
3746
4541
  hookEvent.attrValue = value;
@@ -3751,74 +4546,53 @@ function createDOMPurify() {
3751
4546
  /* Full DOM Clobbering protection via namespace isolation,
3752
4547
  * Prefix id and name attributes with `user-content-`
3753
4548
  */
3754
- if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
4549
+ if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
3755
4550
  // Remove the attribute with this value
3756
4551
  _removeAttribute(name, currentNode);
3757
4552
  // Prefix the value and later re-create the attribute with the sanitized value
3758
4553
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
3759
4554
  }
4555
+ // Else: already prefixed, leave the attribute alone — the prefix is
4556
+ // itself the clobbering protection, and re-applying it is incorrect.
3760
4557
  /* Work around a security issue with comments inside attributes */
3761
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) {
4558
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
3762
4559
  _removeAttribute(name, currentNode);
3763
4560
  continue;
3764
4561
  }
3765
- /* Did the hooks approve of the attribute? */
4562
+ /* Make sure we cannot easily use animated hrefs, even if animations are allowed */
4563
+ if (lcName === 'attributename' && stringMatch(value, 'href')) {
4564
+ _removeAttribute(name, currentNode);
4565
+ continue;
4566
+ }
4567
+ /* Did the hooks force-keep the attribute? */
3766
4568
  if (hookEvent.forceKeepAttr) {
3767
4569
  continue;
3768
4570
  }
3769
- /* Remove attribute */
3770
- _removeAttribute(name, currentNode);
3771
4571
  /* Did the hooks approve of the attribute? */
3772
4572
  if (!hookEvent.keepAttr) {
4573
+ _removeAttribute(name, currentNode);
3773
4574
  continue;
3774
4575
  }
3775
4576
  /* Work around a security issue in jQuery 3.0 */
3776
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
4577
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
3777
4578
  _removeAttribute(name, currentNode);
3778
4579
  continue;
3779
4580
  }
3780
4581
  /* Sanitize attribute content to be template-safe */
3781
4582
  if (SAFE_FOR_TEMPLATES) {
3782
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
3783
- value = stringReplace(value, expr, ' ');
3784
- });
4583
+ value = _stripTemplateExpressions(value);
3785
4584
  }
3786
4585
  /* Is `value` valid for this attribute? */
3787
- const lcTag = transformCaseFunc(currentNode.nodeName);
3788
4586
  if (!_isValidAttribute(lcTag, lcName, value)) {
4587
+ _removeAttribute(name, currentNode);
3789
4588
  continue;
3790
4589
  }
3791
4590
  /* Handle attributes that require Trusted Types */
3792
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
3793
- if (namespaceURI) ; else {
3794
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
3795
- case 'TrustedHTML':
3796
- {
3797
- value = trustedTypesPolicy.createHTML(value);
3798
- break;
3799
- }
3800
- case 'TrustedScriptURL':
3801
- {
3802
- value = trustedTypesPolicy.createScriptURL(value);
3803
- break;
3804
- }
3805
- }
3806
- }
3807
- }
4591
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
3808
4592
  /* Handle invalid data-* attribute set by try-catching it */
3809
- try {
3810
- if (namespaceURI) {
3811
- currentNode.setAttributeNS(namespaceURI, name, value);
3812
- } else {
3813
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
3814
- currentNode.setAttribute(name, value);
3815
- }
3816
- if (_isClobbered(currentNode)) {
3817
- _forceRemove(currentNode);
3818
- } else {
3819
- arrayPop(DOMPurify.removed);
3820
- }
3821
- } catch (_) {}
4593
+ if (value !== initValue) {
4594
+ _setAttributeValue(currentNode, name, namespaceURI, value);
4595
+ }
3822
4596
  }
3823
4597
  /* Execute a hook if present */
3824
4598
  _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
@@ -3828,7 +4602,7 @@ function createDOMPurify() {
3828
4602
  *
3829
4603
  * @param fragment to iterate over recursively
3830
4604
  */
3831
- const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
4605
+ const _sanitizeShadowDOM2 = function _sanitizeShadowDOM(fragment) {
3832
4606
  let shadowNode = null;
3833
4607
  const shadowIterator = _createNodeIterator(fragment);
3834
4608
  /* Execute a hook if present */
@@ -3840,14 +4614,133 @@ function createDOMPurify() {
3840
4614
  _sanitizeElements(shadowNode);
3841
4615
  /* Check attributes next */
3842
4616
  _sanitizeAttributes(shadowNode);
3843
- /* Deep shadow DOM detected */
3844
- if (shadowNode.content instanceof DocumentFragment) {
3845
- _sanitizeShadowDOM(shadowNode.content);
4617
+ /* Deep shadow DOM detected.
4618
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
4619
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
4620
+ recurse into <template>.content from foreign realms too. */
4621
+ if (_isDocumentFragment(shadowNode.content)) {
4622
+ _sanitizeShadowDOM2(shadowNode.content);
4623
+ }
4624
+ /* An element iterated here may itself host an attached
4625
+ shadow root. The default NodeIterator does not enter shadow
4626
+ trees, so a shadow root nested inside template.content was
4627
+ previously reached by no walk at all (the pre-pass at
4628
+ _sanitizeAttachedShadowRoots descends via childNodes, which
4629
+ doesn't enter template.content; the template-content recursion
4630
+ above iterates the content but never inspected shadowRoot).
4631
+ Walk it explicitly. The nodeType guard avoids reading
4632
+ shadowRoot off text / comment / CDATA / PI nodes that the
4633
+ iterator also surfaces. */
4634
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
4635
+ if (shadowNodeType === NODE_TYPE.element) {
4636
+ const innerSr = getShadowRoot(shadowNode);
4637
+ if (_isDocumentFragment(innerSr)) {
4638
+ _sanitizeAttachedShadowRoots(innerSr);
4639
+ _sanitizeShadowDOM2(innerSr);
4640
+ }
3846
4641
  }
3847
4642
  }
3848
4643
  /* Execute a hook if present */
3849
4644
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
3850
4645
  };
4646
+ /**
4647
+ * _sanitizeAttachedShadowRoots
4648
+ *
4649
+ * Walks `root` and feeds every attached shadow root we encounter into
4650
+ * the existing _sanitizeShadowDOM pipeline. The default node iterator
4651
+ * does not descend into shadow trees, so nodes inside an attached
4652
+ * shadow root would otherwise be skipped entirely.
4653
+ *
4654
+ * Two real input paths put attached shadow roots in front of us:
4655
+ * 1. IN_PLACE on a DOM node that already has shadow roots attached.
4656
+ * 2. DOM-node input where importNode(dirty, true) deep-clones the
4657
+ * shadow root because it was created with `clonable: true`.
4658
+ *
4659
+ * This pass runs once, up front, so the main iteration loop (and the
4660
+ * existing _sanitizeShadowDOM template-content recursion) stay
4661
+ * untouched — string-input paths are not affected.
4662
+ *
4663
+ * @param root the subtree root to walk for attached shadow roots
4664
+ */
4665
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots(root) {
4666
+ /* Iterative (explicit stack) rather than per-child recursion. DOM APIs
4667
+ impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
4668
+ built straight into the DOM — the IN_PLACE surface) deeper than the JS
4669
+ call-stack budget would otherwise overflow native recursion here and
4670
+ throw at the IN_PLACE entry pre-pass, before a single node is
4671
+ sanitized, leaving the caller's live tree untouched (fail-open). See
4672
+ campaign-3 F4. A heap stack keeps depth off the call stack.
4673
+ Each work item is either a node to descend into, or a deferred
4674
+ `_sanitizeShadowDOM` for an already-walked shadow root. The deferred
4675
+ form preserves the original post-order discipline: a shadow root's
4676
+ nested shadow roots are discovered before the outer shadow is
4677
+ sanitized (which may remove hosts). Pushes are in reverse of the
4678
+ desired processing order (LIFO): template content, then children, then
4679
+ the shadow-sanitize, then the shadow walk — so the order matches the
4680
+ previous recursion exactly. */
4681
+ const stack = [{
4682
+ node: root,
4683
+ shadow: null
4684
+ }];
4685
+ while (stack.length > 0) {
4686
+ const item = stack.pop();
4687
+ /* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
4688
+ if (item.shadow) {
4689
+ _sanitizeShadowDOM2(item.shadow);
4690
+ continue;
4691
+ }
4692
+ const node = item.node;
4693
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
4694
+ const isElement = nodeType === NODE_TYPE.element;
4695
+ /* (pushed last → processed first) Children, snapshotted in reverse so
4696
+ the first child is processed first. Snapshotting matters because a
4697
+ hook may detach siblings mid-walk. */
4698
+ const childNodes = getChildNodes(node);
4699
+ if (childNodes) {
4700
+ for (let i = childNodes.length - 1; i >= 0; --i) {
4701
+ stack.push({
4702
+ node: childNodes[i],
4703
+ shadow: null
4704
+ });
4705
+ }
4706
+ }
4707
+ /* (pushed before children → processed after them, matching the old
4708
+ "template content last" order) When the node is a <template>,
4709
+ descend into its content. */
4710
+ if (isElement) {
4711
+ const rootName = getNodeName ? getNodeName(node) : null;
4712
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
4713
+ const content = node.content;
4714
+ if (_isDocumentFragment(content)) {
4715
+ stack.push({
4716
+ node: content,
4717
+ shadow: null
4718
+ });
4719
+ }
4720
+ }
4721
+ }
4722
+ /* Shadow root (processed first): walk its subtree, then sanitise it.
4723
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
4724
+ rather than `instanceof DocumentFragment`, which is realm-bound and
4725
+ silently skipped foreign-realm shadow roots (e.g.
4726
+ iframe.contentDocument attachShadow). */
4727
+ if (isElement) {
4728
+ const sr = getShadowRoot(node);
4729
+ if (_isDocumentFragment(sr)) {
4730
+ /* Push the deferred sanitise first so it pops after the shadow
4731
+ walk we push next, i.e. nested shadow roots are discovered
4732
+ before this one is sanitised. */
4733
+ stack.push({
4734
+ node: null,
4735
+ shadow: sr
4736
+ }, {
4737
+ node: sr,
4738
+ shadow: null
4739
+ });
4740
+ }
4741
+ }
4742
+ }
4743
+ };
3851
4744
  // eslint-disable-next-line complexity
3852
4745
  DOMPurify.sanitize = function (dirty) {
3853
4746
  let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -3864,13 +4757,9 @@ function createDOMPurify() {
3864
4757
  }
3865
4758
  /* Stringify, in case dirty is an object */
3866
4759
  if (typeof dirty !== 'string' && !_isNode(dirty)) {
3867
- if (typeof dirty.toString === 'function') {
3868
- dirty = dirty.toString();
3869
- if (typeof dirty !== 'string') {
3870
- throw typeErrorCreate('dirty is not a string, aborting');
3871
- }
3872
- } else {
3873
- throw typeErrorCreate('toString is not a function');
4760
+ dirty = stringifyValue(dirty);
4761
+ if (typeof dirty !== 'string') {
4762
+ throw typeErrorCreate('dirty is not a string, aborting');
3874
4763
  }
3875
4764
  }
3876
4765
  /* Return dirty HTML if DOMPurify cannot run */
@@ -3878,24 +4767,78 @@ function createDOMPurify() {
3878
4767
  return dirty;
3879
4768
  }
3880
4769
  /* Assign config vars */
3881
- if (!SET_CONFIG) {
4770
+ if (SET_CONFIG) {
4771
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
4772
+ * not re-derived per call. Restore them from the pristine bindings
4773
+ * captured at setConfig() time so a previous call's hook clone (mutated
4774
+ * below) does not carry over. */
4775
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
4776
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
4777
+ } else {
3882
4778
  _parseConfig(cfg);
3883
4779
  }
4780
+ /* Clone the hook-mutable allowlists before the walk whenever an
4781
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
4782
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
4783
+ * a hook that widens them would otherwise mutate the shared set
4784
+ * permanently: across later calls and across every element. Cloning per
4785
+ * walk keeps documented in-call widening working while scoping it to the
4786
+ * call. A single guard for both config paths - the per-call path rebinds
4787
+ * the sets in _parseConfig each call, the persistent path restores them
4788
+ * from the captured bindings just above - so the two cannot diverge. */
4789
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
4790
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
4791
+ }
4792
+ if (hooks.uponSanitizeAttribute.length > 0) {
4793
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
4794
+ }
3884
4795
  /* Clean up removed elements */
3885
4796
  DOMPurify.removed = [];
3886
- /* Check if dirty is correctly typed for IN_PLACE */
3887
- if (typeof dirty === 'string') {
3888
- IN_PLACE = false;
3889
- }
3890
- if (IN_PLACE) {
3891
- /* Do some early pre-sanitization to avoid unsafe root nodes */
3892
- if (dirty.nodeName) {
3893
- const tagName = transformCaseFunc(dirty.nodeName);
4797
+ /* Resolve IN_PLACE for this call without mutating persistent config.
4798
+ Writing the IN_PLACE closure variable here leaks under setConfig(),
4799
+ where _parseConfig is skipped on later calls: a single string call would
4800
+ disable in-place mode for every subsequent node call, returning a
4801
+ sanitized copy while leaving the caller's node — which in-place callers
4802
+ keep using and whose return value they ignore unsanitized. REPORT-2. */
4803
+ const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
4804
+ if (inPlace) {
4805
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
4806
+ Read nodeName through the cached prototype getter — a clobbering
4807
+ child named "nodeName" on the form root would otherwise shadow
4808
+ the property and let this check skip the root-allowlist
4809
+ validation entirely. */
4810
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
4811
+ if (typeof nn === 'string') {
4812
+ const tagName = transformCaseFunc(nn);
3894
4813
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
3895
4814
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
3896
4815
  }
3897
4816
  }
3898
- } else if (dirty instanceof Node) {
4817
+ /* Pre-flight the root through _isClobbered. The iterator-driven
4818
+ removal path can not detach a parent-less root: _forceRemove
4819
+ falls through to Element.prototype.remove(), which per spec
4820
+ is a no-op on a node with no parent. A clobbered root would
4821
+ then survive the main loop with its attributes uninspected,
4822
+ because _sanitizeAttributes early-returns on _isClobbered. The
4823
+ result would be an attacker-controlled form, complete with any
4824
+ event-handler attributes the caller passed in, handed back to
4825
+ the application unsanitized. Refuse to sanitize such a root
4826
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
4827
+ if (_isClobbered(dirty)) {
4828
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
4829
+ }
4830
+ /* Sanitize attached shadow roots before the main iterator runs.
4831
+ The iterator does not descend into shadow trees. Same fail-closed
4832
+ barrier as the main walk (campaign-3 F2): a custom-element reaction
4833
+ inside a shadow root could abort this pre-pass before the walk runs,
4834
+ which would otherwise leave the entire live tree unsanitized. */
4835
+ try {
4836
+ _sanitizeAttachedShadowRoots(dirty);
4837
+ } catch (error) {
4838
+ _neutralizeRoot(dirty);
4839
+ throw error;
4840
+ }
4841
+ } else if (_isNode(dirty)) {
3899
4842
  /* If dirty is a DOM element, append to an empty document to avoid
3900
4843
  elements being stripped by the parser */
3901
4844
  body = _initDocument('<!---->');
@@ -3909,12 +4852,18 @@ function createDOMPurify() {
3909
4852
  // eslint-disable-next-line unicorn/prefer-dom-node-append
3910
4853
  body.appendChild(importedNode);
3911
4854
  }
4855
+ /* Clonable shadow roots are deep-cloned by importNode(); sanitize
4856
+ them before the main iterator runs, since the iterator does not
4857
+ descend into shadow trees. The walk routes every read through a
4858
+ cached prototype getter so clobbering descendants on a form root
4859
+ cannot hide a shadow host from this pass. */
4860
+ _sanitizeAttachedShadowRoots(importedNode);
3912
4861
  } else {
3913
4862
  /* Exit directly if we have nothing to do */
3914
4863
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
3915
4864
  // eslint-disable-next-line unicorn/prefer-includes
3916
4865
  dirty.indexOf('<') === -1) {
3917
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
4866
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
3918
4867
  }
3919
4868
  /* Initialize the document to work on */
3920
4869
  body = _initDocument(dirty);
@@ -3928,24 +4877,60 @@ function createDOMPurify() {
3928
4877
  _forceRemove(body.firstChild);
3929
4878
  }
3930
4879
  /* Get node iterator */
3931
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
3932
- /* Now start iterating over the created document */
3933
- while (currentNode = nodeIterator.nextNode()) {
3934
- /* Sanitize tags and elements */
3935
- _sanitizeElements(currentNode);
3936
- /* Check attributes next */
3937
- _sanitizeAttributes(currentNode);
3938
- /* Shadow DOM detected, sanitize it */
3939
- if (currentNode.content instanceof DocumentFragment) {
3940
- _sanitizeShadowDOM(currentNode.content);
4880
+ const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
4881
+ /* Now start iterating over the created document.
4882
+ The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
4883
+ engine/custom-element mutation can detach a node mid-walk so
4884
+ `_forceRemove`'s parentless guard throws, aborting the loop. Without the
4885
+ barrier the caller's in-place tree would be left half-sanitized with the
4886
+ unvisited tail still armed. On any throw we fail closed — strip the
4887
+ in-place root bare then rethrow so the existing throw contract is
4888
+ preserved. (String/DOM-copy paths never return the partial body, so the
4889
+ propagating throw is already fail-closed there.) */
4890
+ try {
4891
+ while (currentNode = nodeIterator.nextNode()) {
4892
+ /* Sanitize tags and elements */
4893
+ _sanitizeElements(currentNode);
4894
+ /* Check attributes next */
4895
+ _sanitizeAttributes(currentNode);
4896
+ /* Shadow DOM detected, sanitize it.
4897
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
4898
+ instead of instanceof, so foreign-realm <template>.content is
4899
+ walked correctly. */
4900
+ if (_isDocumentFragment(currentNode.content)) {
4901
+ _sanitizeShadowDOM2(currentNode.content);
4902
+ }
3941
4903
  }
4904
+ } catch (error) {
4905
+ if (inPlace) {
4906
+ _neutralizeRoot(dirty);
4907
+ }
4908
+ throw error;
3942
4909
  }
3943
4910
  /* If we sanitized `dirty` in-place, return it. */
3944
- if (IN_PLACE) {
4911
+ if (inPlace) {
4912
+ /* Fail-closed completion of the audit-5 F1 fix: every node removed from
4913
+ the caller's live tree is detached but may still hold a queued
4914
+ resource-event handler that fires in page scope after we return. The
4915
+ move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
4916
+ non-allow-listed attributes off every other removed subtree (clobber,
4917
+ mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
4918
+ cancelled before any event can fire. Runs synchronously, pre-return. */
4919
+ arrayForEach(DOMPurify.removed, entry => {
4920
+ if (entry.element) {
4921
+ _neutralizeSubtree(entry.element);
4922
+ }
4923
+ });
4924
+ if (SAFE_FOR_TEMPLATES) {
4925
+ _scrubTemplateExpressions2(dirty);
4926
+ }
3945
4927
  return dirty;
3946
4928
  }
3947
4929
  /* Return sanitized string or DOM */
3948
4930
  if (RETURN_DOM) {
4931
+ if (SAFE_FOR_TEMPLATES) {
4932
+ _scrubTemplateExpressions2(body);
4933
+ }
3949
4934
  if (RETURN_DOM_FRAGMENT) {
3950
4935
  returnNode = createDocumentFragment.call(body.ownerDocument);
3951
4936
  while (body.firstChild) {
@@ -3974,20 +4959,28 @@ function createDOMPurify() {
3974
4959
  }
3975
4960
  /* Sanitize final string template-safe */
3976
4961
  if (SAFE_FOR_TEMPLATES) {
3977
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
3978
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
3979
- });
4962
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
3980
4963
  }
3981
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
4964
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
3982
4965
  };
3983
4966
  DOMPurify.setConfig = function () {
3984
4967
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
3985
4968
  _parseConfig(cfg);
3986
4969
  SET_CONFIG = true;
4970
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
4971
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
3987
4972
  };
3988
4973
  DOMPurify.clearConfig = function () {
3989
4974
  CONFIG = null;
3990
4975
  SET_CONFIG = false;
4976
+ SET_CONFIG_ALLOWED_TAGS = null;
4977
+ SET_CONFIG_ALLOWED_ATTR = null;
4978
+ // Drop any caller-supplied Trusted Types policy so it cannot poison later
4979
+ // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
4980
+ // never recreated — Trusted Types throws on duplicate names) is restored by
4981
+ // the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
4982
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
4983
+ emptyHTML = '';
3991
4984
  };
3992
4985
  DOMPurify.isValidAttribute = function (tag, attr, value) {
3993
4986
  /* Initialize shared config vars if necessary. */
@@ -4002,9 +4995,19 @@ function createDOMPurify() {
4002
4995
  if (typeof hookFunction !== 'function') {
4003
4996
  return;
4004
4997
  }
4998
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
4999
+ * '__proto__') indexes off the prototype chain rather than a real
5000
+ * hook array, and arrayPush then writes to Object.prototype. Guard
5001
+ * with an own-property check against the known hook names. */
5002
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
5003
+ return;
5004
+ }
4005
5005
  arrayPush(hooks[entryPoint], hookFunction);
4006
5006
  };
4007
5007
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
5008
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
5009
+ return undefined;
5010
+ }
4008
5011
  if (hookFunction !== undefined) {
4009
5012
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
4010
5013
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -4012,6 +5015,9 @@ function createDOMPurify() {
4012
5015
  return arrayPop(hooks[entryPoint]);
4013
5016
  };
4014
5017
  DOMPurify.removeHooks = function (entryPoint) {
5018
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
5019
+ return;
5020
+ }
4015
5021
  hooks[entryPoint] = [];
4016
5022
  };
4017
5023
  DOMPurify.removeAllHooks = function () {
@@ -4095,7 +5101,7 @@ try {
4095
5101
  // swallow
4096
5102
  }
4097
5103
  const trusted = createPolicy('trusted', policyOptions);
4098
- /*! version: 0.28.3 */
5104
+ /*! version: 0.28.4 */
4099
5105
 
4100
5106
  /*!
4101
5107
  * Copyright (C) 2019 salesforce.com, inc.
@@ -4404,7 +5410,7 @@ function blobSanitizer$LWS(sandboxKey$LWS) {
4404
5410
  }
4405
5411
  return getSanitizerForConfig$LWS(sandboxKey$LWS, 'STRING_BLOB_HTML');
4406
5412
  }
4407
- /*! version: 0.28.3 */
5413
+ /*! version: 0.28.4 */
4408
5414
 
4409
5415
  /*!
4410
5416
  * Copyright (C) 2023 salesforce.com, inc.
@@ -4613,7 +5619,7 @@ function createScriptSrcURLSetter$LWS(targetElement$LWS) {
4613
5619
  ReflectApply$LWS$1(ElementProtoSetAttributeNS$LWS, targetElement$LWS, [attributeNamespaceURI$LWS, attributeName$LWS, src$LWS]);
4614
5620
  };
4615
5621
  }
4616
- /*! version: 0.28.3 */
5622
+ /*! version: 0.28.4 */
4617
5623
 
4618
5624
  /*!
4619
5625
  * Copyright (C) 2019 salesforce.com, inc.
@@ -14018,7 +15024,7 @@ const SVGElementProtoBlockedProperties$LWS = ['nonce'];
14018
15024
  const UIEventProtoBlockedProperties$LWS = ['rangeParent'];
14019
15025
  const WindowBlockedProperties$LWS = ['find', 'requestFileSystem', 'webkitRequestFileSystem'];
14020
15026
  const XSLTProcessorProtoBlockedProperties$LWS = ['transformToDocument', 'transformToFragment'];
14021
- /*! version: 0.28.3 */
15027
+ /*! version: 0.28.4 */
14022
15028
 
14023
15029
  /*!
14024
15030
  * Copyright (C) 2021 salesforce.com, inc.
@@ -14027,7 +15033,7 @@ let pdpSchema$LWS;
14027
15033
  function getPdpSchema$LWS() {
14028
15034
  return pdpSchema$LWS;
14029
15035
  }
14030
- /*! version: 0.28.3 */
15036
+ /*! version: 0.28.4 */
14031
15037
 
14032
15038
  /*!
14033
15039
  * Copyright (C) 2019 salesforce.com, inc.
@@ -19421,7 +20427,7 @@ function toSourceText$LWS(value$LWS, sourceType$LWS) {
19421
20427
  // tools from mistaking the regexp or the replacement string for an
19422
20428
  // actual source mapping URL.
19423
20429
  /\/\/# sandbox(?=MappingURL=.*?\s*$)/, '//# source']);
19424
- sourceText$LWS = `\n//# LWS Version = "0.28.3"\n${sourceText$LWS}`;
20430
+ sourceText$LWS = `\n//# LWS Version = "0.28.4"\n${sourceText$LWS}`;
19425
20431
  return sourceType$LWS === 1 /* SourceType.Module */ && indexOfPragma$LWS(sourceText$LWS, 'use strict') === -1 ?
19426
20432
  // Append "'use strict'" to the extracted function body so it is
19427
20433
  // evaluated in strict mode.
@@ -20255,7 +21261,7 @@ function wrapPlatformResourceLoader$LWS(dep$LWS, key$LWS) {
20255
21261
  depRegistry$LWS.set(dep$LWS, secureDep$LWS);
20256
21262
  return secureDep$LWS;
20257
21263
  }
20258
- /*! version: 0.28.3 */
21264
+ /*! version: 0.28.4 */
20259
21265
 
20260
21266
  export { $LWS, CORE_SANDBOX_KEY$LWS as CORE_SANDBOX_KEY, createRootWindowSandboxRecord$LWS as createRootWindowSandboxRecord, evaluateFunction$LWS as evaluateFunction, evaluateInCoreSandbox$LWS as evaluateInCoreSandbox, evaluateInSandbox$LWS as evaluateInSandbox, trusted, wrapDependency$LWS as wrapDependency };
20261
21267
  //# sourceMappingURL=lockerSandbox.js.map