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/src/purify.ts CHANGED
@@ -1,10 +1,5 @@
1
- /* eslint-disable @typescript-eslint/indent */
2
-
3
- import type {
4
- TrustedHTML,
5
- TrustedTypesWindow,
6
- } from 'trusted-types/lib/index.js';
7
1
  import type { Config, UseProfilesConfig } from './config';
2
+ import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
8
3
  import * as TAGS from './tags.js';
9
4
  import * as ATTRS from './attrs.js';
10
5
  import * as EXPRESSIONS from './regexp.js';
@@ -13,6 +8,7 @@ import {
13
8
  clone,
14
9
  entries,
15
10
  freeze,
11
+ seal,
16
12
  arrayForEach,
17
13
  arrayIsArray,
18
14
  arrayLastIndexOf,
@@ -36,6 +32,21 @@ import {
36
32
 
37
33
  export type { Config } from './config';
38
34
 
35
+ export type {
36
+ DOMPurify,
37
+ RemovedElement,
38
+ RemovedAttribute,
39
+ HookName,
40
+ NodeHook,
41
+ ElementHook,
42
+ DocumentFragmentHook,
43
+ UponSanitizeElementHook,
44
+ UponSanitizeAttributeHook,
45
+ UponSanitizeElementHookEvent,
46
+ UponSanitizeAttributeHookEvent,
47
+ WindowLike,
48
+ } from './types';
49
+
39
50
  declare const VERSION: string;
40
51
 
41
52
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
@@ -46,7 +57,7 @@ const NODE_TYPE = {
46
57
  cdataSection: 4,
47
58
  entityReference: 5, // Deprecated
48
59
  entityNode: 6, // Deprecated
49
- progressingInstruction: 7,
60
+ processingInstruction: 7,
50
61
  comment: 8,
51
62
  document: 9,
52
63
  documentType: 10,
@@ -122,6 +133,36 @@ const _createHooksMap = function (): HooksMap {
122
133
  };
123
134
  };
124
135
 
136
+ /**
137
+ * Resolve a set-valued configuration option: a fresh set built from
138
+ * cfg[key] when it is an own array property (seeded with a clone of
139
+ * options.base when given, case-normalized via options.transform),
140
+ * the fallback set otherwise.
141
+ *
142
+ * @param cfg the cloned, prototype-free configuration object
143
+ * @param key the configuration property to read
144
+ * @param fallback the set to use when the option is absent or not an array
145
+ * @param options transform and optional base set to merge into
146
+ * @returns the resolved set
147
+ */
148
+ const _resolveSetOption = function (
149
+ cfg: Config,
150
+ key: keyof Config,
151
+ fallback: Record<string, boolean>,
152
+ options: {
153
+ transform: Parameters<typeof addToSet>[2];
154
+ base?: Record<string, boolean>;
155
+ }
156
+ ): Record<string, boolean> {
157
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])
158
+ ? addToSet(
159
+ options.base ? clone(options.base) : {},
160
+ cfg[key] as readonly unknown[],
161
+ options.transform
162
+ )
163
+ : fallback;
164
+ };
165
+
125
166
  function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
126
167
  const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);
127
168
 
@@ -392,6 +433,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
392
433
  /* Track whether config is already set on this instance of DOMPurify. */
393
434
  let SET_CONFIG = false;
394
435
 
436
+ /* Pristine allowlist bindings captured at setConfig() time. On the
437
+ * persistent-config path sanitize() restores the sets from these before
438
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
439
+ * carry across calls. Null until setConfig() is called; reset by
440
+ * clearConfig(). */
441
+ let SET_CONFIG_ALLOWED_TAGS = null;
442
+ let SET_CONFIG_ALLOWED_ATTR = null;
443
+
395
444
  /* Decide if all elements (e.g. style, script) must be children of
396
445
  * document.body. By default, browsers might move them to document.head */
397
446
  let FORCE_BODY = false;
@@ -526,15 +575,20 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
526
575
  stringToString
527
576
  );
528
577
 
529
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [
578
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze([
530
579
  'mi',
531
580
  'mo',
532
581
  'mn',
533
582
  'ms',
534
583
  'mtext',
535
584
  ]);
585
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet(
586
+ {},
587
+ DEFAULT_MATHML_TEXT_INTEGRATION_POINTS
588
+ );
536
589
 
537
- let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
590
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
591
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
538
592
 
539
593
  // Certain elements are allowed in both SVG and HTML
540
594
  // namespace. We need to specify them explicitly
@@ -600,52 +654,48 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
600
654
  : stringToLowerCase;
601
655
 
602
656
  /* Set configuration parameters */
603
- ALLOWED_TAGS =
604
- objectHasOwnProperty(cfg, 'ALLOWED_TAGS') &&
605
- arrayIsArray(cfg.ALLOWED_TAGS)
606
- ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)
607
- : DEFAULT_ALLOWED_TAGS;
608
- ALLOWED_ATTR =
609
- objectHasOwnProperty(cfg, 'ALLOWED_ATTR') &&
610
- arrayIsArray(cfg.ALLOWED_ATTR)
611
- ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)
612
- : DEFAULT_ALLOWED_ATTR;
613
- ALLOWED_NAMESPACES =
614
- objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') &&
615
- arrayIsArray(cfg.ALLOWED_NAMESPACES)
616
- ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)
617
- : DEFAULT_ALLOWED_NAMESPACES;
618
- URI_SAFE_ATTRIBUTES =
619
- objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') &&
620
- arrayIsArray(cfg.ADD_URI_SAFE_ATTR)
621
- ? addToSet(
622
- clone(DEFAULT_URI_SAFE_ATTRIBUTES),
623
- cfg.ADD_URI_SAFE_ATTR,
624
- transformCaseFunc
625
- )
626
- : DEFAULT_URI_SAFE_ATTRIBUTES;
627
- DATA_URI_TAGS =
628
- objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') &&
629
- arrayIsArray(cfg.ADD_DATA_URI_TAGS)
630
- ? addToSet(
631
- clone(DEFAULT_DATA_URI_TAGS),
632
- cfg.ADD_DATA_URI_TAGS,
633
- transformCaseFunc
634
- )
635
- : DEFAULT_DATA_URI_TAGS;
636
- FORBID_CONTENTS =
637
- objectHasOwnProperty(cfg, 'FORBID_CONTENTS') &&
638
- arrayIsArray(cfg.FORBID_CONTENTS)
639
- ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)
640
- : DEFAULT_FORBID_CONTENTS;
641
- FORBID_TAGS =
642
- objectHasOwnProperty(cfg, 'FORBID_TAGS') && arrayIsArray(cfg.FORBID_TAGS)
643
- ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)
644
- : clone({});
645
- FORBID_ATTR =
646
- objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR)
647
- ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)
648
- : clone({});
657
+ ALLOWED_TAGS = _resolveSetOption(
658
+ cfg,
659
+ 'ALLOWED_TAGS',
660
+ DEFAULT_ALLOWED_TAGS,
661
+ { transform: transformCaseFunc }
662
+ );
663
+ ALLOWED_ATTR = _resolveSetOption(
664
+ cfg,
665
+ 'ALLOWED_ATTR',
666
+ DEFAULT_ALLOWED_ATTR,
667
+ { transform: transformCaseFunc }
668
+ );
669
+ ALLOWED_NAMESPACES = _resolveSetOption(
670
+ cfg,
671
+ 'ALLOWED_NAMESPACES',
672
+ DEFAULT_ALLOWED_NAMESPACES,
673
+ { transform: stringToString }
674
+ );
675
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(
676
+ cfg,
677
+ 'ADD_URI_SAFE_ATTR',
678
+ DEFAULT_URI_SAFE_ATTRIBUTES,
679
+ { transform: transformCaseFunc, base: DEFAULT_URI_SAFE_ATTRIBUTES }
680
+ );
681
+ DATA_URI_TAGS = _resolveSetOption(
682
+ cfg,
683
+ 'ADD_DATA_URI_TAGS',
684
+ DEFAULT_DATA_URI_TAGS,
685
+ { transform: transformCaseFunc, base: DEFAULT_DATA_URI_TAGS }
686
+ );
687
+ FORBID_CONTENTS = _resolveSetOption(
688
+ cfg,
689
+ 'FORBID_CONTENTS',
690
+ DEFAULT_FORBID_CONTENTS,
691
+ { transform: transformCaseFunc }
692
+ );
693
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
694
+ transform: transformCaseFunc,
695
+ });
696
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
697
+ transform: transformCaseFunc,
698
+ });
649
699
  USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES')
650
700
  ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object'
651
701
  ? clone(cfg.USE_PROFILES)
@@ -679,14 +729,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
679
729
  cfg.MATHML_TEXT_INTEGRATION_POINTS &&
680
730
  typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object'
681
731
  ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS)
682
- : addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); // Default built-in map
732
+ : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
683
733
 
684
734
  HTML_INTEGRATION_POINTS =
685
735
  objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') &&
686
736
  cfg.HTML_INTEGRATION_POINTS &&
687
737
  typeof cfg.HTML_INTEGRATION_POINTS === 'object'
688
738
  ? clone(cfg.HTML_INTEGRATION_POINTS)
689
- : addToSet({}, ['annotation-xml']); // Default built-in map
739
+ : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
690
740
 
691
741
  const customElementHandling =
692
742
  objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') &&
@@ -723,6 +773,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
723
773
  customElementHandling.allowCustomizedBuiltInElements; // Default undefined
724
774
  }
725
775
 
776
+ seal(CUSTOM_ELEMENT_HANDLING);
777
+
726
778
  if (SAFE_FOR_TEMPLATES) {
727
779
  ALLOW_DATA_ATTR = false;
728
780
  }
@@ -898,30 +950,6 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
898
950
  }
899
951
  }
900
952
 
901
- /*
902
- * Mirror the clone-before-mutate pattern already applied above for
903
- * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
904
- * registered AND the set still points at the default constant,
905
- * clone it. The hook then mutates the clone (in-call widening
906
- * still works exactly as documented) and the next default-cfg
907
- * call rebinds to the untouched original via the reassignment at
908
- * the top of this function.
909
- */
910
- if (
911
- (hooks.uponSanitizeElement.length > 0 ||
912
- hooks.uponSanitizeAttribute.length > 0) &&
913
- ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS
914
- ) {
915
- ALLOWED_TAGS = clone(ALLOWED_TAGS);
916
- }
917
-
918
- if (
919
- hooks.uponSanitizeAttribute.length > 0 &&
920
- ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR
921
- ) {
922
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
923
- }
924
-
925
953
  // Prevent further manipulation of configuration.
926
954
  // Not available in IE8, Safari 5, etc.
927
955
  if (freeze) {
@@ -944,6 +972,111 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
944
972
  ...TAGS.mathMlDisallowed,
945
973
  ]);
946
974
 
975
+ /**
976
+ * Namespace rules for an element in the SVG 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 _checkSvgNamespace = function (
984
+ tagName: string,
985
+ parent: { namespaceURI?: string },
986
+ parentTagName: string
987
+ ): boolean {
988
+ // The only way to switch from HTML namespace to SVG
989
+ // is via <svg>. If it happens via any other tag, then
990
+ // it should be killed.
991
+ if (parent.namespaceURI === HTML_NAMESPACE) {
992
+ return tagName === 'svg';
993
+ }
994
+
995
+ // The only way to switch from MathML to SVG is via <svg>
996
+ // if the parent is either <annotation-xml> or a MathML
997
+ // text integration point.
998
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
999
+ return (
1000
+ tagName === 'svg' &&
1001
+ (parentTagName === 'annotation-xml' ||
1002
+ MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
1003
+ );
1004
+ }
1005
+
1006
+ // We only allow elements that are defined in SVG
1007
+ // spec. All others are disallowed in SVG namespace.
1008
+ return Boolean(ALL_SVG_TAGS[tagName]);
1009
+ };
1010
+
1011
+ /**
1012
+ * Namespace rules for an element in the MathML namespace.
1013
+ *
1014
+ * @param tagName the element's lowercase tag name
1015
+ * @param parent the (possibly simulated) parent node
1016
+ * @param parentTagName the parent's lowercase tag name
1017
+ * @returns true if a spec-compliant parser could produce this element
1018
+ */
1019
+ const _checkMathMlNamespace = function (
1020
+ tagName: string,
1021
+ parent: { namespaceURI?: string },
1022
+ parentTagName: string
1023
+ ): boolean {
1024
+ // The only way to switch from HTML namespace to MathML
1025
+ // is via <math>. If it happens via any other tag, then
1026
+ // it should be killed.
1027
+ if (parent.namespaceURI === HTML_NAMESPACE) {
1028
+ return tagName === 'math';
1029
+ }
1030
+
1031
+ // The only way to switch from SVG to MathML is via
1032
+ // <math> and HTML integration points
1033
+ if (parent.namespaceURI === SVG_NAMESPACE) {
1034
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1035
+ }
1036
+
1037
+ // We only allow elements that are defined in MathML
1038
+ // spec. All others are disallowed in MathML namespace.
1039
+ return Boolean(ALL_MATHML_TAGS[tagName]);
1040
+ };
1041
+
1042
+ /**
1043
+ * Namespace rules for an element in the HTML namespace.
1044
+ *
1045
+ * @param tagName the element's lowercase tag name
1046
+ * @param parent the (possibly simulated) parent node
1047
+ * @param parentTagName the parent's lowercase tag name
1048
+ * @returns true if a spec-compliant parser could produce this element
1049
+ */
1050
+ const _checkHtmlNamespace = function (
1051
+ tagName: string,
1052
+ parent: { namespaceURI?: string },
1053
+ parentTagName: string
1054
+ ): boolean {
1055
+ // The only way to switch from SVG to HTML is via
1056
+ // HTML integration points, and from MathML to HTML
1057
+ // is via MathML text integration points
1058
+ if (
1059
+ parent.namespaceURI === SVG_NAMESPACE &&
1060
+ !HTML_INTEGRATION_POINTS[parentTagName]
1061
+ ) {
1062
+ return false;
1063
+ }
1064
+
1065
+ if (
1066
+ parent.namespaceURI === MATHML_NAMESPACE &&
1067
+ !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
1068
+ ) {
1069
+ return false;
1070
+ }
1071
+
1072
+ // We disallow tags that are specific for MathML
1073
+ // or SVG and should never appear in HTML namespace
1074
+ return (
1075
+ !ALL_MATHML_TAGS[tagName] &&
1076
+ (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
1077
+ );
1078
+ };
1079
+
947
1080
  /**
948
1081
  * @param element a DOM element whose namespace is being checked
949
1082
  * @returns Return false if the element has a
@@ -970,72 +1103,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
970
1103
  }
971
1104
 
972
1105
  if (element.namespaceURI === SVG_NAMESPACE) {
973
- // The only way to switch from HTML namespace to SVG
974
- // is via <svg>. If it happens via any other tag, then
975
- // it should be killed.
976
- if (parent.namespaceURI === HTML_NAMESPACE) {
977
- return tagName === 'svg';
978
- }
979
-
980
- // The only way to switch from MathML to SVG is via`
981
- // svg if parent is either <annotation-xml> or MathML
982
- // text integration points.
983
- if (parent.namespaceURI === MATHML_NAMESPACE) {
984
- return (
985
- tagName === 'svg' &&
986
- (parentTagName === 'annotation-xml' ||
987
- MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
988
- );
989
- }
990
-
991
- // We only allow elements that are defined in SVG
992
- // spec. All others are disallowed in SVG namespace.
993
- return Boolean(ALL_SVG_TAGS[tagName]);
1106
+ return _checkSvgNamespace(tagName, parent, parentTagName);
994
1107
  }
995
1108
 
996
1109
  if (element.namespaceURI === MATHML_NAMESPACE) {
997
- // The only way to switch from HTML namespace to MathML
998
- // is via <math>. If it happens via any other tag, then
999
- // it should be killed.
1000
- if (parent.namespaceURI === HTML_NAMESPACE) {
1001
- return tagName === 'math';
1002
- }
1003
-
1004
- // The only way to switch from SVG to MathML is via
1005
- // <math> and HTML integration points
1006
- if (parent.namespaceURI === SVG_NAMESPACE) {
1007
- return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1008
- }
1009
-
1010
- // We only allow elements that are defined in MathML
1011
- // spec. All others are disallowed in MathML namespace.
1012
- return Boolean(ALL_MATHML_TAGS[tagName]);
1110
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
1013
1111
  }
1014
1112
 
1015
1113
  if (element.namespaceURI === HTML_NAMESPACE) {
1016
- // The only way to switch from SVG to HTML is via
1017
- // HTML integration points, and from MathML to HTML
1018
- // is via MathML text integration points
1019
- if (
1020
- parent.namespaceURI === SVG_NAMESPACE &&
1021
- !HTML_INTEGRATION_POINTS[parentTagName]
1022
- ) {
1023
- return false;
1024
- }
1025
-
1026
- if (
1027
- parent.namespaceURI === MATHML_NAMESPACE &&
1028
- !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
1029
- ) {
1030
- return false;
1031
- }
1032
-
1033
- // We disallow tags that are specific for MathML
1034
- // or SVG and should never appear in HTML namespace
1035
- return (
1036
- !ALL_MATHML_TAGS[tagName] &&
1037
- (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
1038
- );
1114
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
1039
1115
  }
1040
1116
 
1041
1117
  // For XHTML and XML documents that support custom namespaces
@@ -1112,9 +1188,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1112
1188
  * @param root the in-place root to empty
1113
1189
  */
1114
1190
  const _neutralizeRoot = function (root: Node): void {
1115
- const childNodes = getChildNodes
1116
- ? getChildNodes(root)
1117
- : (root as Element).childNodes;
1191
+ const childNodes = getChildNodes(root);
1118
1192
  if (childNodes) {
1119
1193
  const snapshot: Node[] = [];
1120
1194
  arrayForEach(childNodes, (child) => {
@@ -1129,7 +1203,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1129
1203
  });
1130
1204
  }
1131
1205
 
1132
- const attributes = getAttributes ? getAttributes(root) : null;
1206
+ const attributes = getAttributes(root);
1133
1207
  if (attributes) {
1134
1208
  for (let i = attributes.length - 1; i >= 0; --i) {
1135
1209
  const attribute = attributes[i];
@@ -1191,9 +1265,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1191
1265
  * @param element the element to strip
1192
1266
  */
1193
1267
  const _stripDisallowedAttributes = function (element: Element): void {
1194
- const attributes = getAttributes
1195
- ? getAttributes(element)
1196
- : element.attributes;
1268
+ const attributes = getAttributes(element);
1197
1269
  if (!attributes) {
1198
1270
  return;
1199
1271
  }
@@ -1246,9 +1318,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1246
1318
  _stripDisallowedAttributes(node as Element);
1247
1319
  }
1248
1320
 
1249
- const childNodes = getChildNodes
1250
- ? getChildNodes(node)
1251
- : (node as Element).childNodes;
1321
+ const childNodes = getChildNodes(node);
1252
1322
  if (childNodes) {
1253
1323
  for (let i = childNodes.length - 1; i >= 0; --i) {
1254
1324
  stack.push(childNodes[i]);
@@ -1350,6 +1420,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1350
1420
  );
1351
1421
  };
1352
1422
 
1423
+ /**
1424
+ * Replace template expression syntax (mustache, ERB, template
1425
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1426
+ * sites. Order matters: mustache, then ERB, then template literal.
1427
+ *
1428
+ * @param value the string to scrub
1429
+ * @returns the scrubbed string
1430
+ */
1431
+ const _stripTemplateExpressions = function (value: string): string {
1432
+ value = stringReplace(value, MUSTACHE_EXPR, ' ');
1433
+ value = stringReplace(value, ERB_EXPR, ' ');
1434
+ value = stringReplace(value, TMPLIT_EXPR, ' ');
1435
+ return value;
1436
+ };
1437
+
1353
1438
  /**
1354
1439
  * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1355
1440
  * character data of an element subtree. Used as the final safety net for
@@ -1384,23 +1469,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1384
1469
 
1385
1470
  let currentNode = walker.nextNode() as CharacterData | null;
1386
1471
  while (currentNode) {
1387
- let data = currentNode.data;
1388
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1389
- data = stringReplace(data, expr, ' ');
1390
- });
1391
- currentNode.data = data;
1472
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1392
1473
  currentNode = walker.nextNode() as CharacterData | null;
1393
1474
  }
1394
1475
 
1395
1476
  // NodeIterator does not descend into <template>.content per the DOM spec,
1396
1477
  // so we must explicitly recurse into each template's content fragment,
1397
1478
  // mirroring the approach used by _sanitizeShadowDOM.
1398
- const templates = node.querySelectorAll?.('template') ?? [];
1399
- arrayForEach(Array.from(templates), (tmpl: HTMLTemplateElement) => {
1400
- if (_isDocumentFragment(tmpl.content)) {
1401
- _scrubTemplateExpressions(tmpl.content as unknown as Element);
1402
- }
1403
- });
1479
+ const templates = node.querySelectorAll?.('template');
1480
+ if (templates) {
1481
+ arrayForEach(templates, (tmpl: HTMLTemplateElement) => {
1482
+ if (_isDocumentFragment(tmpl.content)) {
1483
+ _scrubTemplateExpressions(tmpl.content as unknown as Element);
1484
+ }
1485
+ });
1486
+ }
1404
1487
  };
1405
1488
 
1406
1489
  /**
@@ -1517,52 +1600,34 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1517
1600
  currentNode: Parameters<T>[0],
1518
1601
  data: Parameters<T>[1]
1519
1602
  ): void {
1603
+ if (hooks.length === 0) {
1604
+ return;
1605
+ }
1606
+
1520
1607
  arrayForEach(hooks, (hook: T) => {
1521
1608
  hook.call(DOMPurify, currentNode, data, CONFIG);
1522
1609
  });
1523
1610
  }
1524
1611
 
1525
1612
  /**
1526
- * _sanitizeElements
1613
+ * Structural-threat checks that condemn a node regardless of the
1614
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1615
+ * processing instructions, markup-bearing comments. Pure predicate;
1616
+ * the caller removes. Check order is load-bearing.
1527
1617
  *
1528
- * @protect nodeName
1529
- * @protect textContent
1530
- * @protect removeChild
1531
- * @param currentNode to check for permission to exist
1532
- * @return true if node was killed, false if left alive
1618
+ * @param currentNode the node to inspect
1619
+ * @param tagName the node's transformCaseFunc'd tag name
1620
+ * @return true if the node must be removed
1533
1621
  */
1534
- const _sanitizeElements = function (currentNode: any): boolean {
1535
- let content = null;
1536
-
1537
- /* Execute a hook if present */
1538
- _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1539
-
1540
- /* Check if element is clobbered or can clobber */
1541
- if (_isClobbered(currentNode)) {
1542
- _forceRemove(currentNode);
1543
- return true;
1544
- }
1545
-
1546
- /* Now let's check the element's type and name */
1547
- const tagName = transformCaseFunc(
1548
- getNodeName ? getNodeName(currentNode) : currentNode.nodeName
1549
- );
1550
-
1551
- /* Execute a hook if present */
1552
- _executeHooks(hooks.uponSanitizeElement, currentNode, {
1553
- tagName,
1554
- allowedTags: ALLOWED_TAGS,
1555
- });
1556
-
1622
+ const _isUnsafeNode = function (currentNode: any, tagName: string): boolean {
1557
1623
  /* Detect mXSS attempts abusing namespace confusion */
1558
1624
  if (
1559
1625
  SAFE_FOR_XML &&
1560
1626
  currentNode.hasChildNodes() &&
1561
1627
  !_isNode(currentNode.firstElementChild) &&
1562
- regExpTest(/<[/\w!]/g, currentNode.innerHTML) &&
1563
- regExpTest(/<[/\w!]/g, currentNode.textContent)
1628
+ regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.textContent) &&
1629
+ regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.innerHTML)
1564
1630
  ) {
1565
- _forceRemove(currentNode);
1566
1631
  return true;
1567
1632
  }
1568
1633
 
@@ -1573,13 +1638,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1573
1638
  tagName === 'style' &&
1574
1639
  _isNode(currentNode.firstElementChild)
1575
1640
  ) {
1576
- _forceRemove(currentNode);
1577
1641
  return true;
1578
1642
  }
1579
1643
 
1580
1644
  /* Remove any occurrence of processing instructions */
1581
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
1582
- _forceRemove(currentNode);
1645
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1583
1646
  return true;
1584
1647
  }
1585
1648
 
@@ -1587,39 +1650,47 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1587
1650
  if (
1588
1651
  SAFE_FOR_XML &&
1589
1652
  currentNode.nodeType === NODE_TYPE.comment &&
1590
- regExpTest(/<[/\w]/g, currentNode.data)
1653
+ regExpTest(EXPRESSIONS.COMMENT_MARKUP_PROBE, currentNode.data)
1591
1654
  ) {
1592
- _forceRemove(currentNode);
1593
1655
  return true;
1594
1656
  }
1595
1657
 
1596
- /* Remove element if anything forbids its presence */
1597
- if (
1598
- FORBID_TAGS[tagName] ||
1599
- (!(
1600
- EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
1601
- EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
1602
- ) &&
1603
- !ALLOWED_TAGS[tagName])
1604
- ) {
1605
- /* Check if we have a custom element to handle */
1606
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1607
- if (
1608
- CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
1609
- regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
1610
- ) {
1611
- return false;
1612
- }
1658
+ return false;
1659
+ };
1613
1660
 
1614
- if (
1615
- CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
1616
- CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
1617
- ) {
1618
- return false;
1619
- }
1661
+ /**
1662
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1663
+ * allowed custom elements (false return exits _sanitizeElements
1664
+ * early - namespace/fallback checks and the afterSanitizeElements
1665
+ * hook are intentionally skipped for kept custom elements), else
1666
+ * hoist content per KEEP_CONTENT and remove.
1667
+ *
1668
+ * @param currentNode the disallowed node
1669
+ * @param tagName the node's transformCaseFunc'd tag name
1670
+ * @return true if the node was removed, false if kept
1671
+ */
1672
+ const _sanitizeDisallowedNode = function (
1673
+ currentNode: any,
1674
+ tagName: string
1675
+ ): boolean {
1676
+ /* Check if we have a custom element to handle */
1677
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1678
+ if (
1679
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
1680
+ regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
1681
+ ) {
1682
+ return false;
1683
+ }
1684
+
1685
+ if (
1686
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
1687
+ CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
1688
+ ) {
1689
+ return false;
1620
1690
  }
1691
+ }
1621
1692
 
1622
- /* Keep content except for bad-listed elements.
1693
+ /* Keep content except for bad-listed elements.
1623
1694
  Use the cached prototype getters exclusively — the previous code
1624
1695
  had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1625
1696
  fallbacks, but the cached getters always return the canonical
@@ -1627,14 +1698,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1627
1698
  path was dead in safe cases and a clobbering surface in unsafe
1628
1699
  ones. Falsy cached results stay falsy; the `if (childNodes &&
1629
1700
  parentNode)` check already gates correctly. */
1630
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1631
- const parentNode = getParentNode(currentNode);
1632
- const childNodes = getChildNodes(currentNode);
1701
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1702
+ const parentNode = getParentNode(currentNode);
1703
+ const childNodes = getChildNodes(currentNode);
1633
1704
 
1634
- if (childNodes && parentNode) {
1635
- const childCount = childNodes.length;
1705
+ if (childNodes && parentNode) {
1706
+ const childCount = childNodes.length;
1636
1707
 
1637
- /* In-place: hoist the *original* children so the iterator visits
1708
+ /* In-place: hoist the *original* children so the iterator visits
1638
1709
  and sanitises them through the same allowlist pass as every other
1639
1710
  node. The caller built the tree in the live document, so the
1640
1711
  originals carry already-queued resource events (`<img onerror>`,
@@ -1655,19 +1726,67 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1655
1726
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1656
1727
  valid whether we move (drops the trailing entry) or clone (leaves
1657
1728
  the list intact). */
1658
- for (let i = childCount - 1; i >= 0; --i) {
1659
- const hoisted = IN_PLACE
1660
- ? childNodes[i]
1661
- : cloneNode(childNodes[i], true);
1662
- parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1663
- }
1729
+ for (let i = childCount - 1; i >= 0; --i) {
1730
+ const hoisted = IN_PLACE
1731
+ ? childNodes[i]
1732
+ : cloneNode(childNodes[i], true);
1733
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1664
1734
  }
1665
1735
  }
1736
+ }
1737
+
1738
+ _forceRemove(currentNode);
1739
+ return true;
1740
+ };
1741
+
1742
+ /**
1743
+ * _sanitizeElements
1744
+ *
1745
+ * @protect nodeName
1746
+ * @protect textContent
1747
+ * @protect removeChild
1748
+ * @param currentNode to check for permission to exist
1749
+ * @return true if node was killed, false if left alive
1750
+ */
1751
+ const _sanitizeElements = function (currentNode: any): boolean {
1752
+ /* Execute a hook if present */
1753
+ _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1754
+
1755
+ /* Check if element is clobbered or can clobber */
1756
+ if (_isClobbered(currentNode)) {
1757
+ _forceRemove(currentNode);
1758
+ return true;
1759
+ }
1760
+
1761
+ /* Now let's check the element's type and name */
1762
+ const tagName = transformCaseFunc(
1763
+ getNodeName ? getNodeName(currentNode) : currentNode.nodeName
1764
+ );
1765
+
1766
+ /* Execute a hook if present */
1767
+ _executeHooks(hooks.uponSanitizeElement, currentNode, {
1768
+ tagName,
1769
+ allowedTags: ALLOWED_TAGS,
1770
+ });
1666
1771
 
1772
+ /* Remove mXSS vectors, processing instructions and risky comments */
1773
+ if (_isUnsafeNode(currentNode, tagName)) {
1667
1774
  _forceRemove(currentNode);
1668
1775
  return true;
1669
1776
  }
1670
1777
 
1778
+ /* Remove element if anything forbids its presence */
1779
+ if (
1780
+ FORBID_TAGS[tagName] ||
1781
+ (!(
1782
+ EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
1783
+ EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
1784
+ ) &&
1785
+ !ALLOWED_TAGS[tagName])
1786
+ ) {
1787
+ return _sanitizeDisallowedNode(currentNode, tagName);
1788
+ }
1789
+
1671
1790
  /* Check whether element has a valid namespace.
1672
1791
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1673
1792
  nodeType getter rather than `instanceof Element`, which is realm-
@@ -1685,7 +1804,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1685
1804
  (tagName === 'noscript' ||
1686
1805
  tagName === 'noembed' ||
1687
1806
  tagName === 'noframes') &&
1688
- regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)
1807
+ regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)
1689
1808
  ) {
1690
1809
  _forceRemove(currentNode);
1691
1810
  return true;
@@ -1694,11 +1813,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1694
1813
  /* Sanitize element content to be template-safe */
1695
1814
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1696
1815
  /* Get the element's text content */
1697
- content = currentNode.textContent;
1698
-
1699
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1700
- content = stringReplace(content, expr, ' ');
1701
- });
1816
+ const content = _stripTemplateExpressions(currentNode.textContent);
1702
1817
 
1703
1818
  if (currentNode.textContent !== content) {
1704
1819
  arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
@@ -1749,16 +1864,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1749
1864
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1750
1865
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1751
1866
  We don't need to check the value; it's always URI safe. */
1752
- if (
1753
- ALLOW_DATA_ATTR &&
1754
- !FORBID_ATTR[lcName] &&
1755
- regExpTest(DATA_ATTR, lcName)
1756
- ) {
1867
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR, lcName)) {
1757
1868
  // This attribute is safe
1758
1869
  } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {
1759
1870
  // This attribute is safe
1760
1871
  /* Otherwise, check the name is permitted */
1761
- } else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
1872
+ } else if (!nameIsPermitted) {
1762
1873
  if (
1763
1874
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1764
1875
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1852,6 +1963,85 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1852
1963
  );
1853
1964
  };
1854
1965
 
1966
+ /**
1967
+ * Wrap an attribute value in the matching Trusted Types object when
1968
+ * the active policy requires it. Namespaced attributes pass through
1969
+ * unchanged (no TT support yet, see
1970
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1971
+ *
1972
+ * @param lcTag lowercase tag name of the containing element
1973
+ * @param lcName lowercase attribute name
1974
+ * @param namespaceURI the attribute's namespace, if any
1975
+ * @param value the attribute value to wrap
1976
+ * @return the value, wrapped when Trusted Types demand it
1977
+ */
1978
+ const _applyTrustedTypesToAttribute = function (
1979
+ lcTag: string,
1980
+ lcName: string,
1981
+ namespaceURI: string | null,
1982
+ value: string
1983
+ ): string {
1984
+ if (
1985
+ trustedTypesPolicy &&
1986
+ typeof trustedTypes === 'object' &&
1987
+ typeof trustedTypes.getAttributeType === 'function' &&
1988
+ !namespaceURI
1989
+ ) {
1990
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1991
+ case 'TrustedHTML': {
1992
+ return _createTrustedHTML(value);
1993
+ }
1994
+
1995
+ case 'TrustedScriptURL': {
1996
+ return _createTrustedScriptURL(value);
1997
+ }
1998
+
1999
+ default: {
2000
+ break;
2001
+ }
2002
+ }
2003
+ }
2004
+
2005
+ return value;
2006
+ };
2007
+
2008
+ /**
2009
+ * Write a modified attribute value back onto the element. On
2010
+ * success, re-probe for clobbering introduced by the new value and
2011
+ * remove the element when found; otherwise pop the removal entry
2012
+ * recorded by the earlier _removeAttribute (long-standing pairing
2013
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
2014
+ * failure, remove the attribute instead.
2015
+ *
2016
+ * @param currentNode the element carrying the attribute
2017
+ * @param name the attribute name as present on the element
2018
+ * @param namespaceURI the attribute's namespace, if any
2019
+ * @param value the new attribute value
2020
+ */
2021
+ const _setAttributeValue = function (
2022
+ currentNode: Element,
2023
+ name: string,
2024
+ namespaceURI: string | null,
2025
+ value: string
2026
+ ): void {
2027
+ try {
2028
+ if (namespaceURI) {
2029
+ currentNode.setAttributeNS(namespaceURI, name, value);
2030
+ } else {
2031
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
2032
+ currentNode.setAttribute(name, value);
2033
+ }
2034
+
2035
+ if (_isClobbered(currentNode)) {
2036
+ _forceRemove(currentNode);
2037
+ } else {
2038
+ arrayPop(DOMPurify.removed);
2039
+ }
2040
+ } catch (_) {
2041
+ _removeAttribute(name, currentNode);
2042
+ }
2043
+ };
2044
+
1855
2045
  /**
1856
2046
  * _sanitizeAttributes
1857
2047
  *
@@ -1881,6 +2071,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1881
2071
  forceKeepAttr: undefined,
1882
2072
  };
1883
2073
  let l = attributes.length;
2074
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1884
2075
 
1885
2076
  /* Go backwards over all attributes; safely remove bad ones */
1886
2077
  while (l--) {
@@ -1933,7 +2124,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1933
2124
  continue;
1934
2125
  }
1935
2126
 
1936
- /* Did the hooks approve of the attribute? */
2127
+ /* Did the hooks force-keep the attribute? */
1937
2128
  if (hookEvent.forceKeepAttr) {
1938
2129
  continue;
1939
2130
  }
@@ -1945,70 +2136,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1945
2136
  }
1946
2137
 
1947
2138
  /* Work around a security issue in jQuery 3.0 */
1948
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
2139
+ if (
2140
+ !ALLOW_SELF_CLOSE_IN_ATTR &&
2141
+ regExpTest(EXPRESSIONS.SELF_CLOSING_TAG, value)
2142
+ ) {
1949
2143
  _removeAttribute(name, currentNode);
1950
2144
  continue;
1951
2145
  }
1952
2146
 
1953
2147
  /* Sanitize attribute content to be template-safe */
1954
2148
  if (SAFE_FOR_TEMPLATES) {
1955
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
1956
- value = stringReplace(value, expr, ' ');
1957
- });
2149
+ value = _stripTemplateExpressions(value);
1958
2150
  }
1959
2151
 
1960
2152
  /* Is `value` valid for this attribute? */
1961
- const lcTag = transformCaseFunc(currentNode.nodeName);
1962
2153
  if (!_isValidAttribute(lcTag, lcName, value)) {
1963
2154
  _removeAttribute(name, currentNode);
1964
2155
  continue;
1965
2156
  }
1966
2157
 
1967
2158
  /* Handle attributes that require Trusted Types */
1968
- if (
1969
- trustedTypesPolicy &&
1970
- typeof trustedTypes === 'object' &&
1971
- typeof trustedTypes.getAttributeType === 'function'
1972
- ) {
1973
- if (namespaceURI) {
1974
- /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */
1975
- } else {
1976
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1977
- case 'TrustedHTML': {
1978
- value = _createTrustedHTML(value);
1979
- break;
1980
- }
1981
-
1982
- case 'TrustedScriptURL': {
1983
- value = _createTrustedScriptURL(value);
1984
- break;
1985
- }
1986
-
1987
- default: {
1988
- break;
1989
- }
1990
- }
1991
- }
1992
- }
2159
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1993
2160
 
1994
2161
  /* Handle invalid data-* attribute set by try-catching it */
1995
2162
  if (value !== initValue) {
1996
- try {
1997
- if (namespaceURI) {
1998
- currentNode.setAttributeNS(namespaceURI, name, value);
1999
- } else {
2000
- /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
2001
- currentNode.setAttribute(name, value);
2002
- }
2003
-
2004
- if (_isClobbered(currentNode)) {
2005
- _forceRemove(currentNode);
2006
- } else {
2007
- arrayPop(DOMPurify.removed);
2008
- }
2009
- } catch (_) {
2010
- _removeAttribute(name, currentNode);
2011
- }
2163
+ _setAttributeValue(currentNode, name, namespaceURI, value);
2012
2164
  }
2013
2165
  }
2014
2166
 
@@ -2060,9 +2212,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2060
2212
  ? getNodeType(shadowNode)
2061
2213
  : shadowNode.nodeType;
2062
2214
  if (shadowNodeType === NODE_TYPE.element) {
2063
- const innerSr = getShadowRoot
2064
- ? getShadowRoot(shadowNode)
2065
- : (shadowNode as Element).shadowRoot;
2215
+ const innerSr = getShadowRoot(shadowNode);
2066
2216
  if (_isDocumentFragment(innerSr)) {
2067
2217
  _sanitizeAttachedShadowRoots(innerSr);
2068
2218
  _sanitizeShadowDOM(innerSr);
@@ -2129,9 +2279,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2129
2279
  /* (pushed last → processed first) Children, snapshotted in reverse so
2130
2280
  the first child is processed first. Snapshotting matters because a
2131
2281
  hook may detach siblings mid-walk. */
2132
- const childNodes = getChildNodes
2133
- ? getChildNodes(node)
2134
- : (node as Element).childNodes;
2282
+ const childNodes = getChildNodes(node);
2135
2283
  if (childNodes) {
2136
2284
  for (let i = childNodes.length - 1; i >= 0; --i) {
2137
2285
  stack.push({ node: childNodes[i], shadow: null });
@@ -2160,9 +2308,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2160
2308
  silently skipped foreign-realm shadow roots (e.g.
2161
2309
  iframe.contentDocument attachShadow). */
2162
2310
  if (isElement) {
2163
- const sr = getShadowRoot
2164
- ? getShadowRoot(node)
2165
- : (node as Element).shadowRoot;
2311
+ const sr = getShadowRoot(node);
2166
2312
  if (_isDocumentFragment(sr)) {
2167
2313
  /* Push the deferred sanitise first so it pops after the shadow
2168
2314
  walk we push next, i.e. nested shadow roots are discovered
@@ -2202,10 +2348,37 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2202
2348
  }
2203
2349
 
2204
2350
  /* Assign config vars */
2205
- if (!SET_CONFIG) {
2351
+ if (SET_CONFIG) {
2352
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
2353
+ * not re-derived per call. Restore them from the pristine bindings
2354
+ * captured at setConfig() time so a previous call's hook clone (mutated
2355
+ * below) does not carry over. */
2356
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
2357
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
2358
+ } else {
2206
2359
  _parseConfig(cfg);
2207
2360
  }
2208
2361
 
2362
+ /* Clone the hook-mutable allowlists before the walk whenever an
2363
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2364
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2365
+ * a hook that widens them would otherwise mutate the shared set
2366
+ * permanently: across later calls and across every element. Cloning per
2367
+ * walk keeps documented in-call widening working while scoping it to the
2368
+ * call. A single guard for both config paths - the per-call path rebinds
2369
+ * the sets in _parseConfig each call, the persistent path restores them
2370
+ * from the captured bindings just above - so the two cannot diverge. */
2371
+ if (
2372
+ hooks.uponSanitizeElement.length > 0 ||
2373
+ hooks.uponSanitizeAttribute.length > 0
2374
+ ) {
2375
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2376
+ }
2377
+
2378
+ if (hooks.uponSanitizeAttribute.length > 0) {
2379
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2380
+ }
2381
+
2209
2382
  /* Clean up removed elements */
2210
2383
  DOMPurify.removed = [];
2211
2384
 
@@ -2421,9 +2594,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2421
2594
 
2422
2595
  /* Sanitize final string template-safe */
2423
2596
  if (SAFE_FOR_TEMPLATES) {
2424
- arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
2425
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
2426
- });
2597
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
2427
2598
  }
2428
2599
 
2429
2600
  return trustedTypesPolicy && RETURN_TRUSTED_TYPE
@@ -2434,11 +2605,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2434
2605
  DOMPurify.setConfig = function (cfg = {}) {
2435
2606
  _parseConfig(cfg);
2436
2607
  SET_CONFIG = true;
2608
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2609
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
2437
2610
  };
2438
2611
 
2439
2612
  DOMPurify.clearConfig = function () {
2440
2613
  CONFIG = null;
2441
2614
  SET_CONFIG = false;
2615
+ SET_CONFIG_ALLOWED_TAGS = null;
2616
+ SET_CONFIG_ALLOWED_ATTR = null;
2442
2617
 
2443
2618
  // Drop any caller-supplied Trusted Types policy so it cannot poison later
2444
2619
  // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
@@ -2467,6 +2642,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2467
2642
  return;
2468
2643
  }
2469
2644
 
2645
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2646
+ * '__proto__') indexes off the prototype chain rather than a real
2647
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2648
+ * with an own-property check against the known hook names. */
2649
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2650
+ return;
2651
+ }
2652
+
2470
2653
  arrayPush(hooks[entryPoint], hookFunction);
2471
2654
  };
2472
2655
 
@@ -2474,6 +2657,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2474
2657
  entryPoint: keyof HooksMap,
2475
2658
  hookFunction: HookFunction
2476
2659
  ) {
2660
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2661
+ return undefined;
2662
+ }
2663
+
2477
2664
  if (hookFunction !== undefined) {
2478
2665
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
2479
2666
 
@@ -2486,6 +2673,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2486
2673
  };
2487
2674
 
2488
2675
  DOMPurify.removeHooks = function (entryPoint: keyof HooksMap) {
2676
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2677
+ return;
2678
+ }
2679
+
2489
2680
  hooks[entryPoint] = [];
2490
2681
  };
2491
2682
 
@@ -2497,352 +2688,3 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2497
2688
  }
2498
2689
 
2499
2690
  export default createDOMPurify();
2500
-
2501
- export interface DOMPurify {
2502
- /**
2503
- * Creates a DOMPurify instance using the given window-like object. Defaults to `window`.
2504
- */
2505
- (root?: WindowLike): DOMPurify;
2506
-
2507
- /**
2508
- * Version label, exposed for easier checks
2509
- * if DOMPurify is up to date or not
2510
- */
2511
- version: string;
2512
-
2513
- /**
2514
- * Array of elements that DOMPurify removed during sanitation.
2515
- * Empty if nothing was removed.
2516
- */
2517
- removed: Array<RemovedElement | RemovedAttribute>;
2518
-
2519
- /**
2520
- * Expose whether this browser supports running the full DOMPurify.
2521
- */
2522
- isSupported: boolean;
2523
-
2524
- /**
2525
- * Set the configuration once.
2526
- *
2527
- * @param cfg configuration object
2528
- */
2529
- setConfig(cfg?: Config): void;
2530
-
2531
- /**
2532
- * Removes the configuration.
2533
- */
2534
- clearConfig(): void;
2535
-
2536
- /**
2537
- * Provides core sanitation functionality.
2538
- *
2539
- * @param dirty string or DOM node
2540
- * @param cfg object
2541
- * @returns Sanitized TrustedHTML.
2542
- */
2543
- sanitize(
2544
- dirty: string | Node,
2545
- cfg: Config & { RETURN_TRUSTED_TYPE: true }
2546
- ): TrustedHTML;
2547
-
2548
- /**
2549
- * Provides core sanitation functionality.
2550
- *
2551
- * @param dirty DOM node
2552
- * @param cfg object
2553
- * @returns Sanitized DOM node.
2554
- */
2555
- sanitize(dirty: Node, cfg: Config & { IN_PLACE: true }): Node;
2556
-
2557
- /**
2558
- * Provides core sanitation functionality.
2559
- *
2560
- * @param dirty string or DOM node
2561
- * @param cfg object
2562
- * @returns Sanitized DOM node.
2563
- */
2564
- sanitize(dirty: string | Node, cfg: Config & { RETURN_DOM: true }): Node;
2565
-
2566
- /**
2567
- * Provides core sanitation functionality.
2568
- *
2569
- * @param dirty string or DOM node
2570
- * @param cfg object
2571
- * @returns Sanitized document fragment.
2572
- */
2573
- sanitize(
2574
- dirty: string | Node,
2575
- cfg: Config & { RETURN_DOM_FRAGMENT: true }
2576
- ): DocumentFragment;
2577
-
2578
- /**
2579
- * Provides core sanitation functionality.
2580
- *
2581
- * @param dirty string or DOM node
2582
- * @param cfg object
2583
- * @returns Sanitized string.
2584
- */
2585
- sanitize(dirty: string | Node, cfg?: Config): string;
2586
-
2587
- /**
2588
- * Checks if an attribute value is valid.
2589
- * Uses last set config, if any. Otherwise, uses config defaults.
2590
- *
2591
- * @param tag Tag name of containing element.
2592
- * @param attr Attribute name.
2593
- * @param value Attribute value.
2594
- * @returns Returns true if `value` is valid. Otherwise, returns false.
2595
- */
2596
- isValidAttribute(tag: string, attr: string, value: string): boolean;
2597
-
2598
- /**
2599
- * Adds a DOMPurify hook.
2600
- *
2601
- * @param entryPoint entry point for the hook to add
2602
- * @param hookFunction function to execute
2603
- */
2604
- addHook(entryPoint: BasicHookName, hookFunction: NodeHook): void;
2605
-
2606
- /**
2607
- * Adds a DOMPurify hook.
2608
- *
2609
- * @param entryPoint entry point for the hook to add
2610
- * @param hookFunction function to execute
2611
- */
2612
- addHook(entryPoint: ElementHookName, hookFunction: ElementHook): void;
2613
-
2614
- /**
2615
- * Adds a DOMPurify hook.
2616
- *
2617
- * @param entryPoint entry point for the hook to add
2618
- * @param hookFunction function to execute
2619
- */
2620
- addHook(
2621
- entryPoint: DocumentFragmentHookName,
2622
- hookFunction: DocumentFragmentHook
2623
- ): void;
2624
-
2625
- /**
2626
- * Adds a DOMPurify hook.
2627
- *
2628
- * @param entryPoint entry point for the hook to add
2629
- * @param hookFunction function to execute
2630
- */
2631
- addHook(
2632
- entryPoint: 'uponSanitizeElement',
2633
- hookFunction: UponSanitizeElementHook
2634
- ): void;
2635
-
2636
- /**
2637
- * Adds a DOMPurify hook.
2638
- *
2639
- * @param entryPoint entry point for the hook to add
2640
- * @param hookFunction function to execute
2641
- */
2642
- addHook(
2643
- entryPoint: 'uponSanitizeAttribute',
2644
- hookFunction: UponSanitizeAttributeHook
2645
- ): void;
2646
-
2647
- /**
2648
- * Remove a DOMPurify hook at a given entryPoint
2649
- * (pops it from the stack of hooks if hook not specified)
2650
- *
2651
- * @param entryPoint entry point for the hook to remove
2652
- * @param hookFunction optional specific hook to remove
2653
- * @returns removed hook
2654
- */
2655
- removeHook(
2656
- entryPoint: BasicHookName,
2657
- hookFunction?: NodeHook
2658
- ): NodeHook | undefined;
2659
-
2660
- /**
2661
- * Remove a DOMPurify hook at a given entryPoint
2662
- * (pops it from the stack of hooks if hook not specified)
2663
- *
2664
- * @param entryPoint entry point for the hook to remove
2665
- * @param hookFunction optional specific hook to remove
2666
- * @returns removed hook
2667
- */
2668
- removeHook(
2669
- entryPoint: ElementHookName,
2670
- hookFunction?: ElementHook
2671
- ): ElementHook | undefined;
2672
-
2673
- /**
2674
- * Remove a DOMPurify hook at a given entryPoint
2675
- * (pops it from the stack of hooks if hook not specified)
2676
- *
2677
- * @param entryPoint entry point for the hook to remove
2678
- * @param hookFunction optional specific hook to remove
2679
- * @returns removed hook
2680
- */
2681
- removeHook(
2682
- entryPoint: DocumentFragmentHookName,
2683
- hookFunction?: DocumentFragmentHook
2684
- ): DocumentFragmentHook | undefined;
2685
-
2686
- /**
2687
- * Remove a DOMPurify hook at a given entryPoint
2688
- * (pops it from the stack of hooks if hook not specified)
2689
- *
2690
- * @param entryPoint entry point for the hook to remove
2691
- * @param hookFunction optional specific hook to remove
2692
- * @returns removed hook
2693
- */
2694
- removeHook(
2695
- entryPoint: 'uponSanitizeElement',
2696
- hookFunction?: UponSanitizeElementHook
2697
- ): UponSanitizeElementHook | undefined;
2698
-
2699
- /**
2700
- * Remove a DOMPurify hook at a given entryPoint
2701
- * (pops it from the stack of hooks if hook not specified)
2702
- *
2703
- * @param entryPoint entry point for the hook to remove
2704
- * @param hookFunction optional specific hook to remove
2705
- * @returns removed hook
2706
- */
2707
- removeHook(
2708
- entryPoint: 'uponSanitizeAttribute',
2709
- hookFunction?: UponSanitizeAttributeHook
2710
- ): UponSanitizeAttributeHook | undefined;
2711
-
2712
- /**
2713
- * Removes all DOMPurify hooks at a given entryPoint
2714
- *
2715
- * @param entryPoint entry point for the hooks to remove
2716
- */
2717
- removeHooks(entryPoint: HookName): void;
2718
-
2719
- /**
2720
- * Removes all DOMPurify hooks.
2721
- */
2722
- removeAllHooks(): void;
2723
- }
2724
-
2725
- /**
2726
- * An element removed by DOMPurify.
2727
- */
2728
- export interface RemovedElement {
2729
- /**
2730
- * The element that was removed.
2731
- */
2732
- element: Node;
2733
- }
2734
-
2735
- /**
2736
- * An element removed by DOMPurify.
2737
- */
2738
- export interface RemovedAttribute {
2739
- /**
2740
- * The attribute that was removed.
2741
- */
2742
- attribute: Attr | null;
2743
-
2744
- /**
2745
- * The element that the attribute was removed.
2746
- */
2747
- from: Node;
2748
- }
2749
-
2750
- type BasicHookName =
2751
- | 'beforeSanitizeElements'
2752
- | 'afterSanitizeElements'
2753
- | 'uponSanitizeShadowNode';
2754
- type ElementHookName = 'beforeSanitizeAttributes' | 'afterSanitizeAttributes';
2755
- type DocumentFragmentHookName =
2756
- | 'beforeSanitizeShadowDOM'
2757
- | 'afterSanitizeShadowDOM';
2758
- type UponSanitizeElementHookName = 'uponSanitizeElement';
2759
- type UponSanitizeAttributeHookName = 'uponSanitizeAttribute';
2760
-
2761
- interface HooksMap {
2762
- beforeSanitizeElements: NodeHook[];
2763
- afterSanitizeElements: NodeHook[];
2764
- beforeSanitizeShadowDOM: DocumentFragmentHook[];
2765
- uponSanitizeShadowNode: NodeHook[];
2766
- afterSanitizeShadowDOM: DocumentFragmentHook[];
2767
- beforeSanitizeAttributes: ElementHook[];
2768
- afterSanitizeAttributes: ElementHook[];
2769
- uponSanitizeElement: UponSanitizeElementHook[];
2770
- uponSanitizeAttribute: UponSanitizeAttributeHook[];
2771
- }
2772
-
2773
- type ArrayElement<T> = T extends Array<infer U> ? U : never;
2774
-
2775
- type HookFunction = ArrayElement<HooksMap[keyof HooksMap]>;
2776
-
2777
- export type HookName =
2778
- | BasicHookName
2779
- | ElementHookName
2780
- | DocumentFragmentHookName
2781
- | UponSanitizeElementHookName
2782
- | UponSanitizeAttributeHookName;
2783
-
2784
- export type NodeHook = (
2785
- this: DOMPurify,
2786
- currentNode: Node,
2787
- hookEvent: null,
2788
- config: Config
2789
- ) => void;
2790
-
2791
- export type ElementHook = (
2792
- this: DOMPurify,
2793
- currentNode: Element,
2794
- hookEvent: null,
2795
- config: Config
2796
- ) => void;
2797
-
2798
- export type DocumentFragmentHook = (
2799
- this: DOMPurify,
2800
- currentNode: DocumentFragment,
2801
- hookEvent: null,
2802
- config: Config
2803
- ) => void;
2804
-
2805
- export type UponSanitizeElementHook = (
2806
- this: DOMPurify,
2807
- currentNode: Node,
2808
- hookEvent: UponSanitizeElementHookEvent,
2809
- config: Config
2810
- ) => void;
2811
-
2812
- export type UponSanitizeAttributeHook = (
2813
- this: DOMPurify,
2814
- currentNode: Element,
2815
- hookEvent: UponSanitizeAttributeHookEvent,
2816
- config: Config
2817
- ) => void;
2818
-
2819
- export interface UponSanitizeElementHookEvent {
2820
- tagName: string;
2821
- allowedTags: Record<string, boolean>;
2822
- }
2823
-
2824
- export interface UponSanitizeAttributeHookEvent {
2825
- attrName: string;
2826
- attrValue: string;
2827
- keepAttr: boolean;
2828
- allowedAttributes: Record<string, boolean>;
2829
- forceKeepAttr: boolean | undefined;
2830
- }
2831
-
2832
- /**
2833
- * A `Window`-like object containing the properties and types that DOMPurify requires.
2834
- */
2835
- export type WindowLike = Pick<
2836
- typeof globalThis,
2837
- | 'DocumentFragment'
2838
- | 'HTMLTemplateElement'
2839
- | 'Node'
2840
- | 'Element'
2841
- | 'NodeFilter'
2842
- | 'NamedNodeMap'
2843
- | 'HTMLFormElement'
2844
- | 'DOMParser'
2845
- > & {
2846
- document?: Document;
2847
- MozNamedAttrMap?: typeof window.NamedNodeMap;
2848
- } & Pick<TrustedTypesWindow, 'trustedTypes'>;