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