dompurify 3.4.9 → 3.4.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.
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.9 | (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.9/LICENSE */
1
+ /*! @license DOMPurify 3.4.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.11/LICENSE */
2
2
 
3
3
  function _arrayLikeToArray(r, a) {
4
4
  (null == a || a > r.length) && (a = r.length);
@@ -327,8 +327,14 @@ const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205
327
327
  );
328
328
  const DOCTYPE_NAME = seal(/^html$/i);
329
329
  const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
330
+ // Markup-significant character probes used by _sanitizeElements.
331
+ // Shared module-level instances are safe despite the sticky /g flags:
332
+ // unapply() resets lastIndex for RegExp receivers before every call.
333
+ const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
334
+ const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
335
+ const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
336
+ const SELF_CLOSING_TAG = seal(/\/>/i);
330
337
 
331
- /* eslint-disable @typescript-eslint/indent */
332
338
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
333
339
  const NODE_TYPE = {
334
340
  element: 1,
@@ -339,7 +345,7 @@ const NODE_TYPE = {
339
345
  // Deprecated
340
346
  entityNode: 6,
341
347
  // Deprecated
342
- progressingInstruction: 7,
348
+ processingInstruction: 7,
343
349
  comment: 8,
344
350
  document: 9,
345
351
  documentType: 10,
@@ -400,10 +406,25 @@ const _createHooksMap = function _createHooksMap() {
400
406
  uponSanitizeShadowNode: []
401
407
  };
402
408
  };
409
+ /**
410
+ * Resolve a set-valued configuration option: a fresh set built from
411
+ * cfg[key] when it is an own array property (seeded with a clone of
412
+ * options.base when given, case-normalized via options.transform),
413
+ * the fallback set otherwise.
414
+ *
415
+ * @param cfg the cloned, prototype-free configuration object
416
+ * @param key the configuration property to read
417
+ * @param fallback the set to use when the option is absent or not an array
418
+ * @param options transform and optional base set to merge into
419
+ * @returns the resolved set
420
+ */
421
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
422
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
423
+ };
403
424
  function createDOMPurify() {
404
425
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
405
426
  const DOMPurify = root => createDOMPurify(root);
406
- DOMPurify.version = '3.4.9';
427
+ DOMPurify.version = '3.4.11';
407
428
  DOMPurify.removed = [];
408
429
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
409
430
  // Not running in a browser, provide a factory function
@@ -592,6 +613,13 @@ function createDOMPurify() {
592
613
  let WHOLE_DOCUMENT = false;
593
614
  /* Track whether config is already set on this instance of DOMPurify. */
594
615
  let SET_CONFIG = false;
616
+ /* Pristine allowlist bindings captured at setConfig() time. On the
617
+ * persistent-config path sanitize() restores the sets from these before
618
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
619
+ * carry across calls. Null until setConfig() is called; reset by
620
+ * clearConfig(). */
621
+ let SET_CONFIG_ALLOWED_TAGS = null;
622
+ let SET_CONFIG_ALLOWED_ATTR = null;
595
623
  /* Decide if all elements (e.g. style, script) must be children of
596
624
  * document.body. By default, browsers might move them to document.head */
597
625
  let FORCE_BODY = false;
@@ -660,8 +688,10 @@ function createDOMPurify() {
660
688
  /* Allowed XHTML+XML namespaces */
661
689
  let ALLOWED_NAMESPACES = null;
662
690
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
663
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
664
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
691
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
692
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
693
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
694
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
665
695
  // Certain elements are allowed in both SVG and HTML
666
696
  // namespace. We need to specify them explicitly
667
697
  // so that they don't get erroneously deleted from
@@ -703,14 +733,32 @@ function createDOMPurify() {
703
733
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
704
734
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
705
735
  /* Set configuration parameters */
706
- ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
707
- ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') && arrayIsArray(cfg.ALLOWED_ATTR) ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
708
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') && arrayIsArray(cfg.ALLOWED_NAMESPACES) ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
709
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR) ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
710
- DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') && arrayIsArray(cfg.ADD_DATA_URI_TAGS) ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
711
- FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS) ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
712
- FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') && arrayIsArray(cfg.FORBID_TAGS) ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
713
- FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR) ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
736
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
737
+ transform: transformCaseFunc
738
+ });
739
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
740
+ transform: transformCaseFunc
741
+ });
742
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
743
+ transform: stringToString
744
+ });
745
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
746
+ transform: transformCaseFunc,
747
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
748
+ });
749
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
750
+ transform: transformCaseFunc,
751
+ base: DEFAULT_DATA_URI_TAGS
752
+ });
753
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
754
+ transform: transformCaseFunc
755
+ });
756
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
757
+ transform: transformCaseFunc
758
+ });
759
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
760
+ transform: transformCaseFunc
761
+ });
714
762
  USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
715
763
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
716
764
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
@@ -729,8 +777,8 @@ function createDOMPurify() {
729
777
  IN_PLACE = cfg.IN_PLACE || false; // Default false
730
778
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
731
779
  NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
732
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); // Default built-in map
733
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, ['annotation-xml']); // Default built-in map
780
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
781
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
734
782
  const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
735
783
  CUSTOM_ELEMENT_HANDLING = create(null);
736
784
  if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
@@ -742,6 +790,7 @@ function createDOMPurify() {
742
790
  if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
743
791
  CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
744
792
  }
793
+ seal(CUSTOM_ELEMENT_HANDLING);
745
794
  if (SAFE_FOR_TEMPLATES) {
746
795
  ALLOW_DATA_ATTR = false;
747
796
  }
@@ -880,21 +929,6 @@ function createDOMPurify() {
880
929
  emptyHTML = _createTrustedHTML('');
881
930
  }
882
931
  }
883
- /*
884
- * Mirror the clone-before-mutate pattern already applied above for
885
- * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
886
- * registered AND the set still points at the default constant,
887
- * clone it. The hook then mutates the clone (in-call widening
888
- * still works exactly as documented) and the next default-cfg
889
- * call rebinds to the untouched original via the reassignment at
890
- * the top of this function.
891
- */
892
- if ((hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) && ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
893
- ALLOWED_TAGS = clone(ALLOWED_TAGS);
894
- }
895
- if (hooks.uponSanitizeAttribute.length > 0 && ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
896
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
897
- }
898
932
  // Prevent further manipulation of configuration.
899
933
  // Not available in IE8, Safari 5, etc.
900
934
  if (freeze) {
@@ -907,6 +941,77 @@ function createDOMPurify() {
907
941
  * correctly. */
908
942
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
909
943
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
944
+ /**
945
+ * Namespace rules for an element in the SVG namespace.
946
+ *
947
+ * @param tagName the element's lowercase tag name
948
+ * @param parent the (possibly simulated) parent node
949
+ * @param parentTagName the parent's lowercase tag name
950
+ * @returns true if a spec-compliant parser could produce this element
951
+ */
952
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
953
+ // The only way to switch from HTML namespace to SVG
954
+ // is via <svg>. If it happens via any other tag, then
955
+ // it should be killed.
956
+ if (parent.namespaceURI === HTML_NAMESPACE) {
957
+ return tagName === 'svg';
958
+ }
959
+ // The only way to switch from MathML to SVG is via <svg>
960
+ // if the parent is either <annotation-xml> or a MathML
961
+ // text integration point.
962
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
963
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
964
+ }
965
+ // We only allow elements that are defined in SVG
966
+ // spec. All others are disallowed in SVG namespace.
967
+ return Boolean(ALL_SVG_TAGS[tagName]);
968
+ };
969
+ /**
970
+ * Namespace rules for an element in the MathML namespace.
971
+ *
972
+ * @param tagName the element's lowercase tag name
973
+ * @param parent the (possibly simulated) parent node
974
+ * @param parentTagName the parent's lowercase tag name
975
+ * @returns true if a spec-compliant parser could produce this element
976
+ */
977
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
978
+ // The only way to switch from HTML namespace to MathML
979
+ // is via <math>. If it happens via any other tag, then
980
+ // it should be killed.
981
+ if (parent.namespaceURI === HTML_NAMESPACE) {
982
+ return tagName === 'math';
983
+ }
984
+ // The only way to switch from SVG to MathML is via
985
+ // <math> and HTML integration points
986
+ if (parent.namespaceURI === SVG_NAMESPACE) {
987
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
988
+ }
989
+ // We only allow elements that are defined in MathML
990
+ // spec. All others are disallowed in MathML namespace.
991
+ return Boolean(ALL_MATHML_TAGS[tagName]);
992
+ };
993
+ /**
994
+ * Namespace rules for an element in the HTML namespace.
995
+ *
996
+ * @param tagName the element's lowercase tag name
997
+ * @param parent the (possibly simulated) parent node
998
+ * @param parentTagName the parent's lowercase tag name
999
+ * @returns true if a spec-compliant parser could produce this element
1000
+ */
1001
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
1002
+ // The only way to switch from SVG to HTML is via
1003
+ // HTML integration points, and from MathML to HTML
1004
+ // is via MathML text integration points
1005
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
1006
+ return false;
1007
+ }
1008
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
1009
+ return false;
1010
+ }
1011
+ // We disallow tags that are specific for MathML
1012
+ // or SVG and should never appear in HTML namespace
1013
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1014
+ };
910
1015
  /**
911
1016
  * @param element a DOM element whose namespace is being checked
912
1017
  * @returns Return false if the element has a
@@ -929,51 +1034,13 @@ function createDOMPurify() {
929
1034
  return false;
930
1035
  }
931
1036
  if (element.namespaceURI === SVG_NAMESPACE) {
932
- // The only way to switch from HTML namespace to SVG
933
- // is via <svg>. If it happens via any other tag, then
934
- // it should be killed.
935
- if (parent.namespaceURI === HTML_NAMESPACE) {
936
- return tagName === 'svg';
937
- }
938
- // The only way to switch from MathML to SVG is via`
939
- // svg if parent is either <annotation-xml> or MathML
940
- // text integration points.
941
- if (parent.namespaceURI === MATHML_NAMESPACE) {
942
- return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
943
- }
944
- // We only allow elements that are defined in SVG
945
- // spec. All others are disallowed in SVG namespace.
946
- return Boolean(ALL_SVG_TAGS[tagName]);
1037
+ return _checkSvgNamespace(tagName, parent, parentTagName);
947
1038
  }
948
1039
  if (element.namespaceURI === MATHML_NAMESPACE) {
949
- // The only way to switch from HTML namespace to MathML
950
- // is via <math>. If it happens via any other tag, then
951
- // it should be killed.
952
- if (parent.namespaceURI === HTML_NAMESPACE) {
953
- return tagName === 'math';
954
- }
955
- // The only way to switch from SVG to MathML is via
956
- // <math> and HTML integration points
957
- if (parent.namespaceURI === SVG_NAMESPACE) {
958
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
959
- }
960
- // We only allow elements that are defined in MathML
961
- // spec. All others are disallowed in MathML namespace.
962
- return Boolean(ALL_MATHML_TAGS[tagName]);
1040
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
963
1041
  }
964
1042
  if (element.namespaceURI === HTML_NAMESPACE) {
965
- // The only way to switch from SVG to HTML is via
966
- // HTML integration points, and from MathML to HTML
967
- // is via MathML text integration points
968
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
969
- return false;
970
- }
971
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
972
- return false;
973
- }
974
- // We disallow tags that are specific for MathML
975
- // or SVG and should never appear in HTML namespace
976
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1043
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
977
1044
  }
978
1045
  // For XHTML and XML documents that support custom namespaces
979
1046
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -1039,7 +1106,7 @@ function createDOMPurify() {
1039
1106
  * @param root the in-place root to empty
1040
1107
  */
1041
1108
  const _neutralizeRoot = function _neutralizeRoot(root) {
1042
- const childNodes = getChildNodes ? getChildNodes(root) : root.childNodes;
1109
+ const childNodes = getChildNodes(root);
1043
1110
  if (childNodes) {
1044
1111
  const snapshot = [];
1045
1112
  arrayForEach(childNodes, child => {
@@ -1053,7 +1120,7 @@ function createDOMPurify() {
1053
1120
  }
1054
1121
  });
1055
1122
  }
1056
- const attributes = getAttributes ? getAttributes(root) : null;
1123
+ const attributes = getAttributes(root);
1057
1124
  if (attributes) {
1058
1125
  for (let i = attributes.length - 1; i >= 0; --i) {
1059
1126
  const attribute = attributes[i];
@@ -1111,7 +1178,7 @@ function createDOMPurify() {
1111
1178
  * @param element the element to strip
1112
1179
  */
1113
1180
  const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
1114
- const attributes = getAttributes ? getAttributes(element) : element.attributes;
1181
+ const attributes = getAttributes(element);
1115
1182
  if (!attributes) {
1116
1183
  return;
1117
1184
  }
@@ -1158,7 +1225,7 @@ function createDOMPurify() {
1158
1225
  if (nodeType === NODE_TYPE.element) {
1159
1226
  _stripDisallowedAttributes(node);
1160
1227
  }
1161
- const childNodes = getChildNodes ? getChildNodes(node) : node.childNodes;
1228
+ const childNodes = getChildNodes(node);
1162
1229
  if (childNodes) {
1163
1230
  for (let i = childNodes.length - 1; i >= 0; --i) {
1164
1231
  stack.push(childNodes[i]);
@@ -1227,6 +1294,20 @@ function createDOMPurify() {
1227
1294
  // eslint-disable-next-line no-bitwise
1228
1295
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1229
1296
  };
1297
+ /**
1298
+ * Replace template expression syntax (mustache, ERB, template
1299
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1300
+ * sites. Order matters: mustache, then ERB, then template literal.
1301
+ *
1302
+ * @param value the string to scrub
1303
+ * @returns the scrubbed string
1304
+ */
1305
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
1306
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
1307
+ value = stringReplace(value, ERB_EXPR$1, ' ');
1308
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
1309
+ return value;
1310
+ };
1230
1311
  /**
1231
1312
  * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1232
1313
  * character data of an element subtree. Used as the final safety net for
@@ -1247,29 +1328,27 @@ function createDOMPurify() {
1247
1328
  * @param node The root element whose character data should be scrubbed.
1248
1329
  */
1249
1330
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1250
- var _node$querySelectorAl, _node$querySelectorAl2;
1331
+ var _node$querySelectorAl;
1251
1332
  node.normalize();
1252
1333
  const walker = createNodeIterator.call(node.ownerDocument || node, node,
1253
1334
  // eslint-disable-next-line no-bitwise
1254
1335
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1255
1336
  let currentNode = walker.nextNode();
1256
1337
  while (currentNode) {
1257
- let data = currentNode.data;
1258
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1259
- data = stringReplace(data, expr, ' ');
1260
- });
1261
- currentNode.data = data;
1338
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1262
1339
  currentNode = walker.nextNode();
1263
1340
  }
1264
1341
  // NodeIterator does not descend into <template>.content per the DOM spec,
1265
1342
  // so we must explicitly recurse into each template's content fragment,
1266
1343
  // mirroring the approach used by _sanitizeShadowDOM.
1267
- const templates = (_node$querySelectorAl = (_node$querySelectorAl2 = node.querySelectorAll) === null || _node$querySelectorAl2 === void 0 ? void 0 : _node$querySelectorAl2.call(node, 'template')) !== null && _node$querySelectorAl !== void 0 ? _node$querySelectorAl : [];
1268
- arrayForEach(Array.from(templates), tmpl => {
1269
- if (_isDocumentFragment(tmpl.content)) {
1270
- _scrubTemplateExpressions2(tmpl.content);
1271
- }
1272
- });
1344
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
1345
+ if (templates) {
1346
+ arrayForEach(templates, tmpl => {
1347
+ if (_isDocumentFragment(tmpl.content)) {
1348
+ _scrubTemplateExpressions2(tmpl.content);
1349
+ }
1350
+ });
1351
+ }
1273
1352
  };
1274
1353
  /**
1275
1354
  * _isClobbered
@@ -1365,67 +1444,64 @@ function createDOMPurify() {
1365
1444
  }
1366
1445
  };
1367
1446
  function _executeHooks(hooks, currentNode, data) {
1447
+ if (hooks.length === 0) {
1448
+ return;
1449
+ }
1368
1450
  arrayForEach(hooks, hook => {
1369
1451
  hook.call(DOMPurify, currentNode, data, CONFIG);
1370
1452
  });
1371
1453
  }
1372
1454
  /**
1373
- * _sanitizeElements
1455
+ * Structural-threat checks that condemn a node regardless of the
1456
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1457
+ * processing instructions, markup-bearing comments. Pure predicate;
1458
+ * the caller removes. Check order is load-bearing.
1374
1459
  *
1375
- * @protect nodeName
1376
- * @protect textContent
1377
- * @protect removeChild
1378
- * @param currentNode to check for permission to exist
1379
- * @return true if node was killed, false if left alive
1460
+ * @param currentNode the node to inspect
1461
+ * @param tagName the node's transformCaseFunc'd tag name
1462
+ * @return true if the node must be removed
1380
1463
  */
1381
- const _sanitizeElements = function _sanitizeElements(currentNode) {
1382
- let content = null;
1383
- /* Execute a hook if present */
1384
- _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1385
- /* Check if element is clobbered or can clobber */
1386
- if (_isClobbered(currentNode)) {
1387
- _forceRemove(currentNode);
1388
- return true;
1389
- }
1390
- /* Now let's check the element's type and name */
1391
- const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1392
- /* Execute a hook if present */
1393
- _executeHooks(hooks.uponSanitizeElement, currentNode, {
1394
- tagName,
1395
- allowedTags: ALLOWED_TAGS
1396
- });
1464
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1397
1465
  /* Detect mXSS attempts abusing namespace confusion */
1398
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
1399
- _forceRemove(currentNode);
1466
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
1400
1467
  return true;
1401
1468
  }
1402
1469
  /* Remove risky CSS construction leading to mXSS */
1403
1470
  if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1404
- _forceRemove(currentNode);
1405
1471
  return true;
1406
1472
  }
1407
1473
  /* Remove any occurrence of processing instructions */
1408
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
1409
- _forceRemove(currentNode);
1474
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1410
1475
  return true;
1411
1476
  }
1412
1477
  /* Remove any kind of possibly harmful comments */
1413
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
1414
- _forceRemove(currentNode);
1478
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
1415
1479
  return true;
1416
1480
  }
1417
- /* Remove element if anything forbids its presence */
1418
- if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1419
- /* Check if we have a custom element to handle */
1420
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1421
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1422
- return false;
1423
- }
1424
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1425
- return false;
1426
- }
1481
+ return false;
1482
+ };
1483
+ /**
1484
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1485
+ * allowed custom elements (false return exits _sanitizeElements
1486
+ * early - namespace/fallback checks and the afterSanitizeElements
1487
+ * hook are intentionally skipped for kept custom elements), else
1488
+ * hoist content per KEEP_CONTENT and remove.
1489
+ *
1490
+ * @param currentNode the disallowed node
1491
+ * @param tagName the node's transformCaseFunc'd tag name
1492
+ * @return true if the node was removed, false if kept
1493
+ */
1494
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1495
+ /* Check if we have a custom element to handle */
1496
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1497
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1498
+ return false;
1427
1499
  }
1428
- /* Keep content except for bad-listed elements.
1500
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1501
+ return false;
1502
+ }
1503
+ }
1504
+ /* Keep content except for bad-listed elements.
1429
1505
  Use the cached prototype getters exclusively — the previous code
1430
1506
  had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1431
1507
  fallbacks, but the cached getters always return the canonical
@@ -1433,12 +1509,12 @@ function createDOMPurify() {
1433
1509
  path was dead in safe cases and a clobbering surface in unsafe
1434
1510
  ones. Falsy cached results stay falsy; the `if (childNodes &&
1435
1511
  parentNode)` check already gates correctly. */
1436
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1437
- const parentNode = getParentNode(currentNode);
1438
- const childNodes = getChildNodes(currentNode);
1439
- if (childNodes && parentNode) {
1440
- const childCount = childNodes.length;
1441
- /* In-place: hoist the *original* children so the iterator visits
1512
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1513
+ const parentNode = getParentNode(currentNode);
1514
+ const childNodes = getChildNodes(currentNode);
1515
+ if (childNodes && parentNode) {
1516
+ const childCount = childNodes.length;
1517
+ /* In-place: hoist the *original* children so the iterator visits
1442
1518
  and sanitises them through the same allowlist pass as every other
1443
1519
  node. The caller built the tree in the live document, so the
1444
1520
  originals carry already-queued resource events (`<img onerror>`,
@@ -1448,24 +1524,57 @@ function createDOMPurify() {
1448
1524
  root is pre-validated as an allowed tag and so is never the node
1449
1525
  being removed, which keeps `parentNode` inside the iterator root
1450
1526
  and the relocated child inside the serialised tree.
1451
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1527
+ Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1452
1528
  at — and the result serialised from — `body`, so a restrictive
1453
1529
  ALLOWED_TAGS that removes `body` itself must leave its content in
1454
1530
  place, which only cloning does; and those paths parse into an
1455
1531
  inert document, so their discarded originals never had a queued
1456
1532
  event to neutralise.
1457
- `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1533
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1458
1534
  valid whether we move (drops the trailing entry) or clone (leaves
1459
1535
  the list intact). */
1460
- for (let i = childCount - 1; i >= 0; --i) {
1461
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1462
- parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1463
- }
1536
+ for (let i = childCount - 1; i >= 0; --i) {
1537
+ const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1538
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1464
1539
  }
1465
1540
  }
1541
+ }
1542
+ _forceRemove(currentNode);
1543
+ return true;
1544
+ };
1545
+ /**
1546
+ * _sanitizeElements
1547
+ *
1548
+ * @protect nodeName
1549
+ * @protect textContent
1550
+ * @protect removeChild
1551
+ * @param currentNode to check for permission to exist
1552
+ * @return true if node was killed, false if left alive
1553
+ */
1554
+ const _sanitizeElements = function _sanitizeElements(currentNode) {
1555
+ /* Execute a hook if present */
1556
+ _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1557
+ /* Check if element is clobbered or can clobber */
1558
+ if (_isClobbered(currentNode)) {
1559
+ _forceRemove(currentNode);
1560
+ return true;
1561
+ }
1562
+ /* Now let's check the element's type and name */
1563
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1564
+ /* Execute a hook if present */
1565
+ _executeHooks(hooks.uponSanitizeElement, currentNode, {
1566
+ tagName,
1567
+ allowedTags: ALLOWED_TAGS
1568
+ });
1569
+ /* Remove mXSS vectors, processing instructions and risky comments */
1570
+ if (_isUnsafeNode(currentNode, tagName)) {
1466
1571
  _forceRemove(currentNode);
1467
1572
  return true;
1468
1573
  }
1574
+ /* Remove element if anything forbids its presence */
1575
+ if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1576
+ return _sanitizeDisallowedNode(currentNode, tagName);
1577
+ }
1469
1578
  /* Check whether element has a valid namespace.
1470
1579
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1471
1580
  nodeType getter rather than `instanceof Element`, which is realm-
@@ -1478,17 +1587,14 @@ function createDOMPurify() {
1478
1587
  return true;
1479
1588
  }
1480
1589
  /* Make sure that older browsers don't get fallback-tag mXSS */
1481
- if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
1590
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
1482
1591
  _forceRemove(currentNode);
1483
1592
  return true;
1484
1593
  }
1485
1594
  /* Sanitize element content to be template-safe */
1486
1595
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1487
1596
  /* Get the element's text content */
1488
- content = currentNode.textContent;
1489
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1490
- content = stringReplace(content, expr, ' ');
1491
- });
1597
+ const content = _stripTemplateExpressions(currentNode.textContent);
1492
1598
  if (currentNode.textContent !== content) {
1493
1599
  arrayPush(DOMPurify.removed, {
1494
1600
  element: currentNode.cloneNode()
@@ -1523,7 +1629,7 @@ function createDOMPurify() {
1523
1629
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1524
1630
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1525
1631
  We don't need to check the value; it's always URI safe. */
1526
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1632
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
1527
1633
  if (
1528
1634
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1529
1635
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1555,6 +1661,63 @@ function createDOMPurify() {
1555
1661
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1556
1662
  return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1557
1663
  };
1664
+ /**
1665
+ * Wrap an attribute value in the matching Trusted Types object when
1666
+ * the active policy requires it. Namespaced attributes pass through
1667
+ * unchanged (no TT support yet, see
1668
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1669
+ *
1670
+ * @param lcTag lowercase tag name of the containing element
1671
+ * @param lcName lowercase attribute name
1672
+ * @param namespaceURI the attribute's namespace, if any
1673
+ * @param value the attribute value to wrap
1674
+ * @return the value, wrapped when Trusted Types demand it
1675
+ */
1676
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
1677
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
1678
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1679
+ case 'TrustedHTML':
1680
+ {
1681
+ return _createTrustedHTML(value);
1682
+ }
1683
+ case 'TrustedScriptURL':
1684
+ {
1685
+ return _createTrustedScriptURL(value);
1686
+ }
1687
+ }
1688
+ }
1689
+ return value;
1690
+ };
1691
+ /**
1692
+ * Write a modified attribute value back onto the element. On
1693
+ * success, re-probe for clobbering introduced by the new value and
1694
+ * remove the element when found; otherwise pop the removal entry
1695
+ * recorded by the earlier _removeAttribute (long-standing pairing
1696
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
1697
+ * failure, remove the attribute instead.
1698
+ *
1699
+ * @param currentNode the element carrying the attribute
1700
+ * @param name the attribute name as present on the element
1701
+ * @param namespaceURI the attribute's namespace, if any
1702
+ * @param value the new attribute value
1703
+ */
1704
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
1705
+ try {
1706
+ if (namespaceURI) {
1707
+ currentNode.setAttributeNS(namespaceURI, name, value);
1708
+ } else {
1709
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1710
+ currentNode.setAttribute(name, value);
1711
+ }
1712
+ if (_isClobbered(currentNode)) {
1713
+ _forceRemove(currentNode);
1714
+ } else {
1715
+ arrayPop(DOMPurify.removed);
1716
+ }
1717
+ } catch (_) {
1718
+ _removeAttribute(name, currentNode);
1719
+ }
1720
+ };
1558
1721
  /**
1559
1722
  * _sanitizeAttributes
1560
1723
  *
@@ -1581,6 +1744,7 @@ function createDOMPurify() {
1581
1744
  forceKeepAttr: undefined
1582
1745
  };
1583
1746
  let l = attributes.length;
1747
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1584
1748
  /* Go backwards over all attributes; safely remove bad ones */
1585
1749
  while (l--) {
1586
1750
  const attr = attributes[l];
@@ -1618,7 +1782,7 @@ function createDOMPurify() {
1618
1782
  _removeAttribute(name, currentNode);
1619
1783
  continue;
1620
1784
  }
1621
- /* Did the hooks approve of the attribute? */
1785
+ /* Did the hooks force-keep the attribute? */
1622
1786
  if (hookEvent.forceKeepAttr) {
1623
1787
  continue;
1624
1788
  }
@@ -1628,56 +1792,24 @@ function createDOMPurify() {
1628
1792
  continue;
1629
1793
  }
1630
1794
  /* Work around a security issue in jQuery 3.0 */
1631
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
1795
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
1632
1796
  _removeAttribute(name, currentNode);
1633
1797
  continue;
1634
1798
  }
1635
1799
  /* Sanitize attribute content to be template-safe */
1636
1800
  if (SAFE_FOR_TEMPLATES) {
1637
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1638
- value = stringReplace(value, expr, ' ');
1639
- });
1801
+ value = _stripTemplateExpressions(value);
1640
1802
  }
1641
1803
  /* Is `value` valid for this attribute? */
1642
- const lcTag = transformCaseFunc(currentNode.nodeName);
1643
1804
  if (!_isValidAttribute(lcTag, lcName, value)) {
1644
1805
  _removeAttribute(name, currentNode);
1645
1806
  continue;
1646
1807
  }
1647
1808
  /* Handle attributes that require Trusted Types */
1648
- if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
1649
- if (namespaceURI) ; else {
1650
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1651
- case 'TrustedHTML':
1652
- {
1653
- value = _createTrustedHTML(value);
1654
- break;
1655
- }
1656
- case 'TrustedScriptURL':
1657
- {
1658
- value = _createTrustedScriptURL(value);
1659
- break;
1660
- }
1661
- }
1662
- }
1663
- }
1809
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1664
1810
  /* Handle invalid data-* attribute set by try-catching it */
1665
1811
  if (value !== initValue) {
1666
- try {
1667
- if (namespaceURI) {
1668
- currentNode.setAttributeNS(namespaceURI, name, value);
1669
- } else {
1670
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1671
- currentNode.setAttribute(name, value);
1672
- }
1673
- if (_isClobbered(currentNode)) {
1674
- _forceRemove(currentNode);
1675
- } else {
1676
- arrayPop(DOMPurify.removed);
1677
- }
1678
- } catch (_) {
1679
- _removeAttribute(name, currentNode);
1680
- }
1812
+ _setAttributeValue(currentNode, name, namespaceURI, value);
1681
1813
  }
1682
1814
  }
1683
1815
  /* Execute a hook if present */
@@ -1719,7 +1851,7 @@ function createDOMPurify() {
1719
1851
  iterator also surfaces. */
1720
1852
  const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
1721
1853
  if (shadowNodeType === NODE_TYPE.element) {
1722
- const innerSr = getShadowRoot ? getShadowRoot(shadowNode) : shadowNode.shadowRoot;
1854
+ const innerSr = getShadowRoot(shadowNode);
1723
1855
  if (_isDocumentFragment(innerSr)) {
1724
1856
  _sanitizeAttachedShadowRoots(innerSr);
1725
1857
  _sanitizeShadowDOM2(innerSr);
@@ -1781,7 +1913,7 @@ function createDOMPurify() {
1781
1913
  /* (pushed last → processed first) Children, snapshotted in reverse so
1782
1914
  the first child is processed first. Snapshotting matters because a
1783
1915
  hook may detach siblings mid-walk. */
1784
- const childNodes = getChildNodes ? getChildNodes(node) : node.childNodes;
1916
+ const childNodes = getChildNodes(node);
1785
1917
  if (childNodes) {
1786
1918
  for (let i = childNodes.length - 1; i >= 0; --i) {
1787
1919
  stack.push({
@@ -1811,7 +1943,7 @@ function createDOMPurify() {
1811
1943
  silently skipped foreign-realm shadow roots (e.g.
1812
1944
  iframe.contentDocument attachShadow). */
1813
1945
  if (isElement) {
1814
- const sr = getShadowRoot ? getShadowRoot(node) : node.shadowRoot;
1946
+ const sr = getShadowRoot(node);
1815
1947
  if (_isDocumentFragment(sr)) {
1816
1948
  /* Push the deferred sanitise first so it pops after the shadow
1817
1949
  walk we push next, i.e. nested shadow roots are discovered
@@ -1853,9 +1985,31 @@ function createDOMPurify() {
1853
1985
  return dirty;
1854
1986
  }
1855
1987
  /* Assign config vars */
1856
- if (!SET_CONFIG) {
1988
+ if (SET_CONFIG) {
1989
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
1990
+ * not re-derived per call. Restore them from the pristine bindings
1991
+ * captured at setConfig() time so a previous call's hook clone (mutated
1992
+ * below) does not carry over. */
1993
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
1994
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
1995
+ } else {
1857
1996
  _parseConfig(cfg);
1858
1997
  }
1998
+ /* Clone the hook-mutable allowlists before the walk whenever an
1999
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2000
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2001
+ * a hook that widens them would otherwise mutate the shared set
2002
+ * permanently: across later calls and across every element. Cloning per
2003
+ * walk keeps documented in-call widening working while scoping it to the
2004
+ * call. A single guard for both config paths - the per-call path rebinds
2005
+ * the sets in _parseConfig each call, the persistent path restores them
2006
+ * from the captured bindings just above - so the two cannot diverge. */
2007
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
2008
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2009
+ }
2010
+ if (hooks.uponSanitizeAttribute.length > 0) {
2011
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2012
+ }
1859
2013
  /* Clean up removed elements */
1860
2014
  DOMPurify.removed = [];
1861
2015
  /* Resolve IN_PLACE for this call without mutating persistent config.
@@ -2023,9 +2177,7 @@ function createDOMPurify() {
2023
2177
  }
2024
2178
  /* Sanitize final string template-safe */
2025
2179
  if (SAFE_FOR_TEMPLATES) {
2026
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
2027
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
2028
- });
2180
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
2029
2181
  }
2030
2182
  return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
2031
2183
  };
@@ -2033,10 +2185,14 @@ function createDOMPurify() {
2033
2185
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
2034
2186
  _parseConfig(cfg);
2035
2187
  SET_CONFIG = true;
2188
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2189
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
2036
2190
  };
2037
2191
  DOMPurify.clearConfig = function () {
2038
2192
  CONFIG = null;
2039
2193
  SET_CONFIG = false;
2194
+ SET_CONFIG_ALLOWED_TAGS = null;
2195
+ SET_CONFIG_ALLOWED_ATTR = null;
2040
2196
  // Drop any caller-supplied Trusted Types policy so it cannot poison later
2041
2197
  // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
2042
2198
  // never recreated — Trusted Types throws on duplicate names) is restored by
@@ -2057,9 +2213,19 @@ function createDOMPurify() {
2057
2213
  if (typeof hookFunction !== 'function') {
2058
2214
  return;
2059
2215
  }
2216
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2217
+ * '__proto__') indexes off the prototype chain rather than a real
2218
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2219
+ * with an own-property check against the known hook names. */
2220
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2221
+ return;
2222
+ }
2060
2223
  arrayPush(hooks[entryPoint], hookFunction);
2061
2224
  };
2062
2225
  DOMPurify.removeHook = function (entryPoint, hookFunction) {
2226
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2227
+ return undefined;
2228
+ }
2063
2229
  if (hookFunction !== undefined) {
2064
2230
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
2065
2231
  return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -2067,6 +2233,9 @@ function createDOMPurify() {
2067
2233
  return arrayPop(hooks[entryPoint]);
2068
2234
  };
2069
2235
  DOMPurify.removeHooks = function (entryPoint) {
2236
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2237
+ return;
2238
+ }
2070
2239
  hooks[entryPoint] = [];
2071
2240
  };
2072
2241
  DOMPurify.removeAllHooks = function () {