dompurify 3.4.9 → 3.4.10

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.10 | (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.10/LICENSE */
2
2
 
3
3
  (function (global, factory) {
4
4
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
@@ -333,6 +333,13 @@
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
344
  /* eslint-disable @typescript-eslint/indent */
338
345
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
@@ -345,7 +352,7 @@
345
352
  // Deprecated
346
353
  entityNode: 6,
347
354
  // Deprecated
348
- progressingInstruction: 7,
355
+ processingInstruction: 7,
349
356
  comment: 8,
350
357
  document: 9,
351
358
  documentType: 10,
@@ -406,10 +413,25 @@
406
413
  uponSanitizeShadowNode: []
407
414
  };
408
415
  };
416
+ /**
417
+ * Resolve a set-valued configuration option: a fresh set built from
418
+ * cfg[key] when it is an own array property (seeded with a clone of
419
+ * options.base when given, case-normalized via options.transform),
420
+ * the fallback set otherwise.
421
+ *
422
+ * @param cfg the cloned, prototype-free configuration object
423
+ * @param key the configuration property to read
424
+ * @param fallback the set to use when the option is absent or not an array
425
+ * @param options transform and optional base set to merge into
426
+ * @returns the resolved set
427
+ */
428
+ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
429
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
430
+ };
409
431
  function createDOMPurify() {
410
432
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
411
433
  const DOMPurify = root => createDOMPurify(root);
412
- DOMPurify.version = '3.4.9';
434
+ DOMPurify.version = '3.4.10';
413
435
  DOMPurify.removed = [];
414
436
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
415
437
  // Not running in a browser, provide a factory function
@@ -666,8 +688,10 @@
666
688
  /* Allowed XHTML+XML namespaces */
667
689
  let ALLOWED_NAMESPACES = null;
668
690
  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']);
691
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
692
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
693
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
694
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
671
695
  // Certain elements are allowed in both SVG and HTML
672
696
  // namespace. We need to specify them explicitly
673
697
  // so that they don't get erroneously deleted from
@@ -709,14 +733,32 @@
709
733
  // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
710
734
  transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
711
735
  /* 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({});
736
+ ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
737
+ transform: transformCaseFunc
738
+ });
739
+ ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
740
+ transform: transformCaseFunc
741
+ });
742
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
743
+ transform: stringToString
744
+ });
745
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
746
+ transform: transformCaseFunc,
747
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
748
+ });
749
+ DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
750
+ transform: transformCaseFunc,
751
+ base: DEFAULT_DATA_URI_TAGS
752
+ });
753
+ FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
754
+ transform: transformCaseFunc
755
+ });
756
+ FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
757
+ transform: transformCaseFunc
758
+ });
759
+ FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
760
+ transform: transformCaseFunc
761
+ });
720
762
  USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
721
763
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
722
764
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
@@ -735,8 +777,8 @@
735
777
  IN_PLACE = cfg.IN_PLACE || false; // Default false
736
778
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
737
779
  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
780
+ MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
781
+ HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
740
782
  const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
741
783
  CUSTOM_ELEMENT_HANDLING = create(null);
742
784
  if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
@@ -748,6 +790,7 @@
748
790
  if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
749
791
  CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
750
792
  }
793
+ seal(CUSTOM_ELEMENT_HANDLING);
751
794
  if (SAFE_FOR_TEMPLATES) {
752
795
  ALLOW_DATA_ATTR = false;
753
796
  }
@@ -913,6 +956,77 @@
913
956
  * correctly. */
914
957
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
915
958
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
959
+ /**
960
+ * Namespace rules for an element in the SVG namespace.
961
+ *
962
+ * @param tagName the element's lowercase tag name
963
+ * @param parent the (possibly simulated) parent node
964
+ * @param parentTagName the parent's lowercase tag name
965
+ * @returns true if a spec-compliant parser could produce this element
966
+ */
967
+ const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
968
+ // The only way to switch from HTML namespace to SVG
969
+ // is via <svg>. If it happens via any other tag, then
970
+ // it should be killed.
971
+ if (parent.namespaceURI === HTML_NAMESPACE) {
972
+ return tagName === 'svg';
973
+ }
974
+ // The only way to switch from MathML to SVG is via <svg>
975
+ // if the parent is either <annotation-xml> or a MathML
976
+ // text integration point.
977
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
978
+ return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
979
+ }
980
+ // We only allow elements that are defined in SVG
981
+ // spec. All others are disallowed in SVG namespace.
982
+ return Boolean(ALL_SVG_TAGS[tagName]);
983
+ };
984
+ /**
985
+ * Namespace rules for an element in the MathML namespace.
986
+ *
987
+ * @param tagName the element's lowercase tag name
988
+ * @param parent the (possibly simulated) parent node
989
+ * @param parentTagName the parent's lowercase tag name
990
+ * @returns true if a spec-compliant parser could produce this element
991
+ */
992
+ const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
993
+ // The only way to switch from HTML namespace to MathML
994
+ // is via <math>. If it happens via any other tag, then
995
+ // it should be killed.
996
+ if (parent.namespaceURI === HTML_NAMESPACE) {
997
+ return tagName === 'math';
998
+ }
999
+ // The only way to switch from SVG to MathML is via
1000
+ // <math> and HTML integration points
1001
+ if (parent.namespaceURI === SVG_NAMESPACE) {
1002
+ return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
1003
+ }
1004
+ // We only allow elements that are defined in MathML
1005
+ // spec. All others are disallowed in MathML namespace.
1006
+ return Boolean(ALL_MATHML_TAGS[tagName]);
1007
+ };
1008
+ /**
1009
+ * Namespace rules for an element in the HTML namespace.
1010
+ *
1011
+ * @param tagName the element's lowercase tag name
1012
+ * @param parent the (possibly simulated) parent node
1013
+ * @param parentTagName the parent's lowercase tag name
1014
+ * @returns true if a spec-compliant parser could produce this element
1015
+ */
1016
+ const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
1017
+ // The only way to switch from SVG to HTML is via
1018
+ // HTML integration points, and from MathML to HTML
1019
+ // is via MathML text integration points
1020
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
1021
+ return false;
1022
+ }
1023
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
1024
+ return false;
1025
+ }
1026
+ // We disallow tags that are specific for MathML
1027
+ // or SVG and should never appear in HTML namespace
1028
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
1029
+ };
916
1030
  /**
917
1031
  * @param element a DOM element whose namespace is being checked
918
1032
  * @returns Return false if the element has a
@@ -935,51 +1049,13 @@
935
1049
  return false;
936
1050
  }
937
1051
  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]);
1052
+ return _checkSvgNamespace(tagName, parent, parentTagName);
953
1053
  }
954
1054
  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]);
1055
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
969
1056
  }
970
1057
  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]);
1058
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
983
1059
  }
984
1060
  // For XHTML and XML documents that support custom namespaces
985
1061
  if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
@@ -1045,7 +1121,7 @@
1045
1121
  * @param root the in-place root to empty
1046
1122
  */
1047
1123
  const _neutralizeRoot = function _neutralizeRoot(root) {
1048
- const childNodes = getChildNodes ? getChildNodes(root) : root.childNodes;
1124
+ const childNodes = getChildNodes(root);
1049
1125
  if (childNodes) {
1050
1126
  const snapshot = [];
1051
1127
  arrayForEach(childNodes, child => {
@@ -1059,7 +1135,7 @@
1059
1135
  }
1060
1136
  });
1061
1137
  }
1062
- const attributes = getAttributes ? getAttributes(root) : null;
1138
+ const attributes = getAttributes(root);
1063
1139
  if (attributes) {
1064
1140
  for (let i = attributes.length - 1; i >= 0; --i) {
1065
1141
  const attribute = attributes[i];
@@ -1117,7 +1193,7 @@
1117
1193
  * @param element the element to strip
1118
1194
  */
1119
1195
  const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
1120
- const attributes = getAttributes ? getAttributes(element) : element.attributes;
1196
+ const attributes = getAttributes(element);
1121
1197
  if (!attributes) {
1122
1198
  return;
1123
1199
  }
@@ -1164,7 +1240,7 @@
1164
1240
  if (nodeType === NODE_TYPE.element) {
1165
1241
  _stripDisallowedAttributes(node);
1166
1242
  }
1167
- const childNodes = getChildNodes ? getChildNodes(node) : node.childNodes;
1243
+ const childNodes = getChildNodes(node);
1168
1244
  if (childNodes) {
1169
1245
  for (let i = childNodes.length - 1; i >= 0; --i) {
1170
1246
  stack.push(childNodes[i]);
@@ -1233,6 +1309,20 @@
1233
1309
  // eslint-disable-next-line no-bitwise
1234
1310
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1235
1311
  };
1312
+ /**
1313
+ * Replace template expression syntax (mustache, ERB, template
1314
+ * literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
1315
+ * sites. Order matters: mustache, then ERB, then template literal.
1316
+ *
1317
+ * @param value the string to scrub
1318
+ * @returns the scrubbed string
1319
+ */
1320
+ const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
1321
+ value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
1322
+ value = stringReplace(value, ERB_EXPR$1, ' ');
1323
+ value = stringReplace(value, TMPLIT_EXPR$1, ' ');
1324
+ return value;
1325
+ };
1236
1326
  /**
1237
1327
  * Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
1238
1328
  * character data of an element subtree. Used as the final safety net for
@@ -1253,29 +1343,27 @@
1253
1343
  * @param node The root element whose character data should be scrubbed.
1254
1344
  */
1255
1345
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1256
- var _node$querySelectorAl, _node$querySelectorAl2;
1346
+ var _node$querySelectorAl;
1257
1347
  node.normalize();
1258
1348
  const walker = createNodeIterator.call(node.ownerDocument || node, node,
1259
1349
  // eslint-disable-next-line no-bitwise
1260
1350
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1261
1351
  let currentNode = walker.nextNode();
1262
1352
  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;
1353
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
1268
1354
  currentNode = walker.nextNode();
1269
1355
  }
1270
1356
  // NodeIterator does not descend into <template>.content per the DOM spec,
1271
1357
  // so we must explicitly recurse into each template's content fragment,
1272
1358
  // 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
- });
1359
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
1360
+ if (templates) {
1361
+ arrayForEach(templates, tmpl => {
1362
+ if (_isDocumentFragment(tmpl.content)) {
1363
+ _scrubTemplateExpressions2(tmpl.content);
1364
+ }
1365
+ });
1366
+ }
1279
1367
  };
1280
1368
  /**
1281
1369
  * _isClobbered
@@ -1371,67 +1459,64 @@
1371
1459
  }
1372
1460
  };
1373
1461
  function _executeHooks(hooks, currentNode, data) {
1462
+ if (hooks.length === 0) {
1463
+ return;
1464
+ }
1374
1465
  arrayForEach(hooks, hook => {
1375
1466
  hook.call(DOMPurify, currentNode, data, CONFIG);
1376
1467
  });
1377
1468
  }
1378
1469
  /**
1379
- * _sanitizeElements
1470
+ * Structural-threat checks that condemn a node regardless of the
1471
+ * allowlists: mXSS via namespace confusion, risky CSS construction,
1472
+ * processing instructions, markup-bearing comments. Pure predicate;
1473
+ * the caller removes. Check order is load-bearing.
1380
1474
  *
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
1475
+ * @param currentNode the node to inspect
1476
+ * @param tagName the node's transformCaseFunc'd tag name
1477
+ * @return true if the node must be removed
1386
1478
  */
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
- });
1479
+ const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1403
1480
  /* 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);
1481
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
1406
1482
  return true;
1407
1483
  }
1408
1484
  /* Remove risky CSS construction leading to mXSS */
1409
1485
  if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1410
- _forceRemove(currentNode);
1411
1486
  return true;
1412
1487
  }
1413
1488
  /* Remove any occurrence of processing instructions */
1414
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
1415
- _forceRemove(currentNode);
1489
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
1416
1490
  return true;
1417
1491
  }
1418
1492
  /* 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);
1493
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
1421
1494
  return true;
1422
1495
  }
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
- }
1496
+ return false;
1497
+ };
1498
+ /**
1499
+ * Handle a node whose tag is forbidden or not allowlisted: keep
1500
+ * allowed custom elements (false return exits _sanitizeElements
1501
+ * early - namespace/fallback checks and the afterSanitizeElements
1502
+ * hook are intentionally skipped for kept custom elements), else
1503
+ * hoist content per KEEP_CONTENT and remove.
1504
+ *
1505
+ * @param currentNode the disallowed node
1506
+ * @param tagName the node's transformCaseFunc'd tag name
1507
+ * @return true if the node was removed, false if kept
1508
+ */
1509
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1510
+ /* Check if we have a custom element to handle */
1511
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1512
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1513
+ return false;
1514
+ }
1515
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1516
+ return false;
1433
1517
  }
1434
- /* Keep content except for bad-listed elements.
1518
+ }
1519
+ /* Keep content except for bad-listed elements.
1435
1520
  Use the cached prototype getters exclusively — the previous code
1436
1521
  had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1437
1522
  fallbacks, but the cached getters always return the canonical
@@ -1439,12 +1524,12 @@
1439
1524
  path was dead in safe cases and a clobbering surface in unsafe
1440
1525
  ones. Falsy cached results stay falsy; the `if (childNodes &&
1441
1526
  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
1527
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1528
+ const parentNode = getParentNode(currentNode);
1529
+ const childNodes = getChildNodes(currentNode);
1530
+ if (childNodes && parentNode) {
1531
+ const childCount = childNodes.length;
1532
+ /* In-place: hoist the *original* children so the iterator visits
1448
1533
  and sanitises them through the same allowlist pass as every other
1449
1534
  node. The caller built the tree in the live document, so the
1450
1535
  originals carry already-queued resource events (`<img onerror>`,
@@ -1454,24 +1539,57 @@
1454
1539
  root is pre-validated as an allowed tag and so is never the node
1455
1540
  being removed, which keeps `parentNode` inside the iterator root
1456
1541
  and the relocated child inside the serialised tree.
1457
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1542
+ Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1458
1543
  at — and the result serialised from — `body`, so a restrictive
1459
1544
  ALLOWED_TAGS that removes `body` itself must leave its content in
1460
1545
  place, which only cloning does; and those paths parse into an
1461
1546
  inert document, so their discarded originals never had a queued
1462
1547
  event to neutralise.
1463
- `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1548
+ `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1464
1549
  valid whether we move (drops the trailing entry) or clone (leaves
1465
1550
  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
- }
1551
+ for (let i = childCount - 1; i >= 0; --i) {
1552
+ const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1553
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1470
1554
  }
1471
1555
  }
1556
+ }
1557
+ _forceRemove(currentNode);
1558
+ return true;
1559
+ };
1560
+ /**
1561
+ * _sanitizeElements
1562
+ *
1563
+ * @protect nodeName
1564
+ * @protect textContent
1565
+ * @protect removeChild
1566
+ * @param currentNode to check for permission to exist
1567
+ * @return true if node was killed, false if left alive
1568
+ */
1569
+ const _sanitizeElements = function _sanitizeElements(currentNode) {
1570
+ /* Execute a hook if present */
1571
+ _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1572
+ /* Check if element is clobbered or can clobber */
1573
+ if (_isClobbered(currentNode)) {
1472
1574
  _forceRemove(currentNode);
1473
1575
  return true;
1474
1576
  }
1577
+ /* Now let's check the element's type and name */
1578
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1579
+ /* Execute a hook if present */
1580
+ _executeHooks(hooks.uponSanitizeElement, currentNode, {
1581
+ tagName,
1582
+ allowedTags: ALLOWED_TAGS
1583
+ });
1584
+ /* Remove mXSS vectors, processing instructions and risky comments */
1585
+ if (_isUnsafeNode(currentNode, tagName)) {
1586
+ _forceRemove(currentNode);
1587
+ return true;
1588
+ }
1589
+ /* Remove element if anything forbids its presence */
1590
+ if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1591
+ return _sanitizeDisallowedNode(currentNode, tagName);
1592
+ }
1475
1593
  /* Check whether element has a valid namespace.
1476
1594
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1477
1595
  nodeType getter rather than `instanceof Element`, which is realm-
@@ -1484,17 +1602,14 @@
1484
1602
  return true;
1485
1603
  }
1486
1604
  /* 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)) {
1605
+ if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
1488
1606
  _forceRemove(currentNode);
1489
1607
  return true;
1490
1608
  }
1491
1609
  /* Sanitize element content to be template-safe */
1492
1610
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
1493
1611
  /* 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
- });
1612
+ const content = _stripTemplateExpressions(currentNode.textContent);
1498
1613
  if (currentNode.textContent !== content) {
1499
1614
  arrayPush(DOMPurify.removed, {
1500
1615
  element: currentNode.cloneNode()
@@ -1529,7 +1644,7 @@
1529
1644
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1530
1645
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1531
1646
  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]) {
1647
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
1533
1648
  if (
1534
1649
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1535
1650
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -1561,6 +1676,63 @@
1561
1676
  const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
1562
1677
  return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
1563
1678
  };
1679
+ /**
1680
+ * Wrap an attribute value in the matching Trusted Types object when
1681
+ * the active policy requires it. Namespaced attributes pass through
1682
+ * unchanged (no TT support yet, see
1683
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
1684
+ *
1685
+ * @param lcTag lowercase tag name of the containing element
1686
+ * @param lcName lowercase attribute name
1687
+ * @param namespaceURI the attribute's namespace, if any
1688
+ * @param value the attribute value to wrap
1689
+ * @return the value, wrapped when Trusted Types demand it
1690
+ */
1691
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
1692
+ if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
1693
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1694
+ case 'TrustedHTML':
1695
+ {
1696
+ return _createTrustedHTML(value);
1697
+ }
1698
+ case 'TrustedScriptURL':
1699
+ {
1700
+ return _createTrustedScriptURL(value);
1701
+ }
1702
+ }
1703
+ }
1704
+ return value;
1705
+ };
1706
+ /**
1707
+ * Write a modified attribute value back onto the element. On
1708
+ * success, re-probe for clobbering introduced by the new value and
1709
+ * remove the element when found; otherwise pop the removal entry
1710
+ * recorded by the earlier _removeAttribute (long-standing pairing
1711
+ * with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
1712
+ * failure, remove the attribute instead.
1713
+ *
1714
+ * @param currentNode the element carrying the attribute
1715
+ * @param name the attribute name as present on the element
1716
+ * @param namespaceURI the attribute's namespace, if any
1717
+ * @param value the new attribute value
1718
+ */
1719
+ const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
1720
+ try {
1721
+ if (namespaceURI) {
1722
+ currentNode.setAttributeNS(namespaceURI, name, value);
1723
+ } else {
1724
+ /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
1725
+ currentNode.setAttribute(name, value);
1726
+ }
1727
+ if (_isClobbered(currentNode)) {
1728
+ _forceRemove(currentNode);
1729
+ } else {
1730
+ arrayPop(DOMPurify.removed);
1731
+ }
1732
+ } catch (_) {
1733
+ _removeAttribute(name, currentNode);
1734
+ }
1735
+ };
1564
1736
  /**
1565
1737
  * _sanitizeAttributes
1566
1738
  *
@@ -1587,6 +1759,7 @@
1587
1759
  forceKeepAttr: undefined
1588
1760
  };
1589
1761
  let l = attributes.length;
1762
+ const lcTag = transformCaseFunc(currentNode.nodeName);
1590
1763
  /* Go backwards over all attributes; safely remove bad ones */
1591
1764
  while (l--) {
1592
1765
  const attr = attributes[l];
@@ -1624,7 +1797,7 @@
1624
1797
  _removeAttribute(name, currentNode);
1625
1798
  continue;
1626
1799
  }
1627
- /* Did the hooks approve of the attribute? */
1800
+ /* Did the hooks force-keep the attribute? */
1628
1801
  if (hookEvent.forceKeepAttr) {
1629
1802
  continue;
1630
1803
  }
@@ -1634,56 +1807,24 @@
1634
1807
  continue;
1635
1808
  }
1636
1809
  /* Work around a security issue in jQuery 3.0 */
1637
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
1810
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
1638
1811
  _removeAttribute(name, currentNode);
1639
1812
  continue;
1640
1813
  }
1641
1814
  /* Sanitize attribute content to be template-safe */
1642
1815
  if (SAFE_FOR_TEMPLATES) {
1643
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
1644
- value = stringReplace(value, expr, ' ');
1645
- });
1816
+ value = _stripTemplateExpressions(value);
1646
1817
  }
1647
1818
  /* Is `value` valid for this attribute? */
1648
- const lcTag = transformCaseFunc(currentNode.nodeName);
1649
1819
  if (!_isValidAttribute(lcTag, lcName, value)) {
1650
1820
  _removeAttribute(name, currentNode);
1651
1821
  continue;
1652
1822
  }
1653
1823
  /* 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
- }
1824
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
1670
1825
  /* Handle invalid data-* attribute set by try-catching it */
1671
1826
  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
- }
1827
+ _setAttributeValue(currentNode, name, namespaceURI, value);
1687
1828
  }
1688
1829
  }
1689
1830
  /* Execute a hook if present */
@@ -1725,7 +1866,7 @@
1725
1866
  iterator also surfaces. */
1726
1867
  const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
1727
1868
  if (shadowNodeType === NODE_TYPE.element) {
1728
- const innerSr = getShadowRoot ? getShadowRoot(shadowNode) : shadowNode.shadowRoot;
1869
+ const innerSr = getShadowRoot(shadowNode);
1729
1870
  if (_isDocumentFragment(innerSr)) {
1730
1871
  _sanitizeAttachedShadowRoots(innerSr);
1731
1872
  _sanitizeShadowDOM2(innerSr);
@@ -1787,7 +1928,7 @@
1787
1928
  /* (pushed last → processed first) Children, snapshotted in reverse so
1788
1929
  the first child is processed first. Snapshotting matters because a
1789
1930
  hook may detach siblings mid-walk. */
1790
- const childNodes = getChildNodes ? getChildNodes(node) : node.childNodes;
1931
+ const childNodes = getChildNodes(node);
1791
1932
  if (childNodes) {
1792
1933
  for (let i = childNodes.length - 1; i >= 0; --i) {
1793
1934
  stack.push({
@@ -1817,7 +1958,7 @@
1817
1958
  silently skipped foreign-realm shadow roots (e.g.
1818
1959
  iframe.contentDocument attachShadow). */
1819
1960
  if (isElement) {
1820
- const sr = getShadowRoot ? getShadowRoot(node) : node.shadowRoot;
1961
+ const sr = getShadowRoot(node);
1821
1962
  if (_isDocumentFragment(sr)) {
1822
1963
  /* Push the deferred sanitise first so it pops after the shadow
1823
1964
  walk we push next, i.e. nested shadow roots are discovered
@@ -2029,9 +2170,7 @@
2029
2170
  }
2030
2171
  /* Sanitize final string template-safe */
2031
2172
  if (SAFE_FOR_TEMPLATES) {
2032
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
2033
- serializedHTML = stringReplace(serializedHTML, expr, ' ');
2034
- });
2173
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
2035
2174
  }
2036
2175
  return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
2037
2176
  };