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