dompurify 3.4.6 → 3.4.8

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.6 | (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.6/LICENSE */
1
+ /*! @license DOMPurify 3.4.8 | (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.8/LICENSE */
2
2
 
3
3
  (function (global, factory) {
4
4
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
@@ -409,7 +409,7 @@
409
409
  function createDOMPurify() {
410
410
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
411
411
  const DOMPurify = root => createDOMPurify(root);
412
- DOMPurify.version = '3.4.6';
412
+ DOMPurify.version = '3.4.8';
413
413
  DOMPurify.removed = [];
414
414
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
415
415
  // Not running in a browser, provide a factory function
@@ -454,6 +454,23 @@
454
454
  }
455
455
  let trustedTypesPolicy;
456
456
  let emptyHTML = '';
457
+ // Tracks whether we are already inside a call to the configured Trusted Types
458
+ // policy's `createHTML`. If the supplied `TRUSTED_TYPES_POLICY.createHTML`
459
+ // itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
460
+ // re-enter the policy and recurse until the stack overflows. We detect that
461
+ // re-entry and throw a clear, actionable error instead.
462
+ let IN_POLICY_CREATE_HTML = 0;
463
+ const _createTrustedHTML = function _createTrustedHTML(html) {
464
+ if (IN_POLICY_CREATE_HTML > 0) {
465
+ throw typeErrorCreate('The configured TRUSTED_TYPES_POLICY.createHTML must not call ' + 'DOMPurify.sanitize, as that causes infinite recursion. Do not pass ' + 'a policy whose createHTML wraps DOMPurify as TRUSTED_TYPES_POLICY; ' + 'see the "DOMPurify and Trusted Types" section of the README.');
466
+ }
467
+ IN_POLICY_CREATE_HTML++;
468
+ try {
469
+ return trustedTypesPolicy.createHTML(html);
470
+ } finally {
471
+ IN_POLICY_CREATE_HTML--;
472
+ }
473
+ };
457
474
  const _document = document,
458
475
  implementation = _document.implementation,
459
476
  createNodeIterator = _document.createNodeIterator,
@@ -781,19 +798,47 @@
781
798
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
782
799
  }
783
800
  // Overwrite existing TrustedTypes policy.
801
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
784
802
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
785
- // Sign local variables required by `sanitize`.
786
- emptyHTML = trustedTypesPolicy.createHTML('');
803
+ // Sign local variables required by `sanitize`. If the supplied policy's
804
+ // `createHTML` is circular (i.e. it calls `DOMPurify.sanitize`), this
805
+ // throws via the re-entrancy guard. Restore the previous policy first so
806
+ // the instance is not left in a poisoned state. See #1422.
807
+ try {
808
+ emptyHTML = _createTrustedHTML('');
809
+ } catch (error) {
810
+ trustedTypesPolicy = previousTrustedTypesPolicy;
811
+ throw error;
812
+ }
787
813
  } else {
788
814
  // Uninitialized policy, attempt to initialize the internal dompurify policy.
789
- if (trustedTypesPolicy === undefined) {
815
+ if (trustedTypesPolicy === undefined && cfg.TRUSTED_TYPES_POLICY !== null) {
790
816
  trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
791
817
  }
792
818
  // If creating the internal policy succeeded sign internal variables.
793
- if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
794
- emptyHTML = trustedTypesPolicy.createHTML('');
819
+ // Note: a falsy `trustedTypesPolicy` (null when policy creation failed or
820
+ // was skipped via `TRUSTED_TYPES_POLICY: null`, or undefined when no
821
+ // policy has been initialized yet) must be excluded here, otherwise we
822
+ // would call `.createHTML` on a non-policy and throw. See #1422.
823
+ if (trustedTypesPolicy && typeof emptyHTML === 'string') {
824
+ emptyHTML = _createTrustedHTML('');
795
825
  }
796
826
  }
827
+ /*
828
+ * Mirror the clone-before-mutate pattern already applied above for
829
+ * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
830
+ * registered AND the set still points at the default constant,
831
+ * clone it. The hook then mutates the clone (in-call widening
832
+ * still works exactly as documented) and the next default-cfg
833
+ * call rebinds to the untouched original via the reassignment at
834
+ * the top of this function.
835
+ */
836
+ if ((hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) && ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
837
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
838
+ }
839
+ if (hooks.uponSanitizeAttribute.length > 0 && ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
840
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
841
+ }
797
842
  // Prevent further manipulation of configuration.
798
843
  // Not available in IE8, Safari 5, etc.
799
844
  if (freeze) {
@@ -953,7 +998,7 @@
953
998
  // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
954
999
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
955
1000
  }
956
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
1001
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
957
1002
  /*
958
1003
  * Use the DOMParser API by default, fallback later if needs be
959
1004
  * DOMParser not work for svg when has multiple root element.
@@ -1012,7 +1057,8 @@
1012
1057
  *
1013
1058
  * @param node The root element whose character data should be scrubbed.
1014
1059
  */
1015
- const _scrubTemplateExpressions = function _scrubTemplateExpressions(node) {
1060
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1061
+ var _node$querySelectorAl, _node$querySelectorAl2;
1016
1062
  node.normalize();
1017
1063
  const walker = createNodeIterator.call(node.ownerDocument || node, node,
1018
1064
  // eslint-disable-next-line no-bitwise
@@ -1026,6 +1072,15 @@
1026
1072
  currentNode.data = data;
1027
1073
  currentNode = walker.nextNode();
1028
1074
  }
1075
+ // NodeIterator does not descend into <template>.content per the DOM spec,
1076
+ // so we must explicitly recurse into each template's content fragment,
1077
+ // mirroring the approach used by _sanitizeShadowDOM.
1078
+ 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 : [];
1079
+ arrayForEach(Array.from(templates), tmpl => {
1080
+ if (_isDocumentFragment(tmpl.content)) {
1081
+ _scrubTemplateExpressions2(tmpl.content);
1082
+ }
1083
+ });
1029
1084
  };
1030
1085
  /**
1031
1086
  * _isClobbered
@@ -1036,32 +1091,6 @@
1036
1091
  * on direct reads. We use this check at the IN_PLACE entry-point and
1037
1092
  * during attribute sanitization to refuse clobbered forms.
1038
1093
  *
1039
- * Realm safety (GHSA-hpcv-96wg-7vj8): every check in this function must
1040
- * work for foreign-realm forms — e.g. a <form> created inside a same-
1041
- * origin iframe and then handed to a parent-realm DOMPurify instance
1042
- * with IN_PLACE: true. The original implementation used
1043
- * `element instanceof HTMLFormElement` and `element.attributes
1044
- * instanceof NamedNodeMap`, both of which are realm-bound: a foreign-
1045
- * realm form is an instance of the *foreign* realm's HTMLFormElement,
1046
- * not the parent realm's. The instanceof short-circuited to false and
1047
- * the function returned false (= not clobbered) regardless of how
1048
- * thoroughly the form was clobbered. Sanitize then walked a clobbered
1049
- * .attributes and missed every attribute on the form root, leaving
1050
- * onmouseover / onclick / formaction / etc. intact.
1051
- *
1052
- * The realm-independent replacements:
1053
- * - HTMLFormElement detection — read the tag name through the cached
1054
- * Node.prototype.nodeName getter. WebIDL getters operate on internal
1055
- * slots that exist on every real Node regardless of which realm
1056
- * minted the JS wrapper, so getNodeName(foreignForm) === "FORM".
1057
- * - NamedNodeMap detection — compare the direct .attributes read
1058
- * against the cached Element.prototype.attributes getter. Same
1059
- * equality-probe pattern we use for .childNodes: if a clobbering
1060
- * child shadows the named property, the two reads diverge; if not,
1061
- * both return the same NamedNodeMap (same-realm OR foreign-realm —
1062
- * doesn't matter, both are the canonical attributes object for the
1063
- * node).
1064
- *
1065
1094
  * @param element element to check for clobbering attacks
1066
1095
  * @return true if clobbered, false if safe
1067
1096
  */
@@ -1083,6 +1112,14 @@
1083
1112
  // (same-realm OR foreign-realm) has both reads pointing at the same
1084
1113
  // canonical NamedNodeMap.
1085
1114
  element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
1115
+ // NodeType clobbering probe. Cached Node.prototype.nodeType getter
1116
+ // returns the integer 1 for any Element regardless of realm; direct
1117
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
1118
+ // the named child element. Cheap addition — nodeType is read from
1119
+ // an internal slot, no serialization cost — and removes a residual
1120
+ // clobbering surface used by several mXSS / PI / comment branches
1121
+ // in _sanitizeElements that compare currentNode.nodeType directly.
1122
+ element.nodeType !== getNodeType(element) ||
1086
1123
  // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
1087
1124
  // "childNodes" shadows the prototype getter. Direct reads of
1088
1125
  // form.childNodes from a clobbered form return the named child
@@ -1099,14 +1136,6 @@
1099
1136
  /**
1100
1137
  * Checks whether the given value is a DocumentFragment from any realm.
1101
1138
  *
1102
- * Realm safety (GHSA-hpcv-96wg-7vj8): the original sites used
1103
- * `value instanceof DocumentFragment`, which is realm-bound — a fragment
1104
- * from a foreign realm (template content or shadow root from an iframe
1105
- * document) is an instance of the foreign realm's DocumentFragment, not
1106
- * the parent realm's, so the check returned false and the template-
1107
- * content / shadow-root recursion was silently skipped. The attacker
1108
- * payload inside survived untouched.
1109
- *
1110
1139
  * The realm-independent replacement reads `nodeType` through the cached
1111
1140
  * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
1112
1141
  * constant (11). nodeType is a numeric value resolved from the node's
@@ -1133,12 +1162,6 @@
1133
1162
  * sanitize() to silently stringify them and reset IN_PLACE to false,
1134
1163
  * returning the original node unsanitized. See GHSA-4w3q-35jp-p934.
1135
1164
  *
1136
- * Implementation: call the cached `nodeType` getter from Node.prototype
1137
- * directly on the value. This bypasses any clobbered instance property
1138
- * (e.g. a child element named "nodeType") and works across realms
1139
- * because the WebIDL `nodeType` getter reads an internal slot that
1140
- * every real Node has, regardless of which window minted it.
1141
- *
1142
1165
  * @param value object to check whether it's a DOM node
1143
1166
  * @return true if value is a DOM node from any realm
1144
1167
  */
@@ -1176,7 +1199,7 @@
1176
1199
  return true;
1177
1200
  }
1178
1201
  /* Now let's check the element's type and name */
1179
- const tagName = transformCaseFunc(currentNode.nodeName);
1202
+ const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1180
1203
  /* Execute a hook if present */
1181
1204
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1182
1205
  tagName,
@@ -1213,10 +1236,17 @@
1213
1236
  return false;
1214
1237
  }
1215
1238
  }
1216
- /* Keep content except for bad-listed elements */
1239
+ /* Keep content except for bad-listed elements.
1240
+ Use the cached prototype getters exclusively — the previous code
1241
+ had `|| currentNode.parentNode` / `|| currentNode.childNodes`
1242
+ fallbacks, but the cached getters always return the canonical
1243
+ value (or null for a real parent-less node), so the fallback
1244
+ path was dead in safe cases and a clobbering surface in unsafe
1245
+ ones. Falsy cached results stay falsy; the `if (childNodes &&
1246
+ parentNode)` check already gates correctly. */
1217
1247
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
1218
- const parentNode = getParentNode(currentNode) || currentNode.parentNode;
1219
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
1248
+ const parentNode = getParentNode(currentNode);
1249
+ const childNodes = getChildNodes(currentNode);
1220
1250
  if (childNodes && parentNode) {
1221
1251
  const childCount = childNodes.length;
1222
1252
  for (let i = childCount - 1; i >= 0; --i) {
@@ -1412,7 +1442,7 @@
1412
1442
  switch (trustedTypes.getAttributeType(lcTag, lcName)) {
1413
1443
  case 'TrustedHTML':
1414
1444
  {
1415
- value = trustedTypesPolicy.createHTML(value);
1445
+ value = _createTrustedHTML(value);
1416
1446
  break;
1417
1447
  }
1418
1448
  case 'TrustedScriptURL':
@@ -1469,6 +1499,24 @@
1469
1499
  if (_isDocumentFragment(shadowNode.content)) {
1470
1500
  _sanitizeShadowDOM2(shadowNode.content);
1471
1501
  }
1502
+ /* An element iterated here may itself host an attached
1503
+ shadow root. The default NodeIterator does not enter shadow
1504
+ trees, so a shadow root nested inside template.content was
1505
+ previously reached by no walk at all (the pre-pass at
1506
+ _sanitizeAttachedShadowRoots descends via childNodes, which
1507
+ doesn't enter template.content; the template-content recursion
1508
+ above iterates the content but never inspected shadowRoot).
1509
+ Walk it explicitly. The nodeType guard avoids reading
1510
+ shadowRoot off text / comment / CDATA / PI nodes that the
1511
+ iterator also surfaces. */
1512
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
1513
+ if (shadowNodeType === NODE_TYPE.element) {
1514
+ const innerSr = getShadowRoot ? getShadowRoot(shadowNode) : shadowNode.shadowRoot;
1515
+ if (_isDocumentFragment(innerSr)) {
1516
+ _sanitizeAttachedShadowRoots2(innerSr);
1517
+ _sanitizeShadowDOM2(innerSr);
1518
+ }
1519
+ }
1472
1520
  }
1473
1521
  /* Execute a hook if present */
1474
1522
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
@@ -1490,17 +1538,6 @@
1490
1538
  * existing _sanitizeShadowDOM template-content recursion) stay
1491
1539
  * untouched — string-input paths are not affected.
1492
1540
  *
1493
- * DOM-Clobbering hardening: HTMLFormElement carries the WebIDL
1494
- * [LegacyOverrideBuiltIns] extended attribute, so a descendant element
1495
- * named `nodeType`, `shadowRoot`, or `childNodes` shadows the matching
1496
- * prototype getter on the form. Reading those properties directly off
1497
- * the node would let an attacker steer this walk past shadow hosts
1498
- * (e.g. <input name="childNodes"> collapses the form's child list to
1499
- * the input itself, so descent stops dead and any shadow root deeper
1500
- * in the subtree is never sanitized). Every property access here is
1501
- * therefore routed through the cached prototype getter; the form's
1502
- * named-property getter cannot intercept those reads.
1503
- *
1504
1541
  * @param root the subtree root to walk for attached shadow roots
1505
1542
  */
1506
1543
  const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
@@ -1536,6 +1573,16 @@
1536
1573
  for (const child of snapshot) {
1537
1574
  _sanitizeAttachedShadowRoots2(child);
1538
1575
  }
1576
+ /* When the root is a <template>, also descend into root.content */
1577
+ if (nodeType === NODE_TYPE.element) {
1578
+ const rootName = getNodeName ? getNodeName(root) : null;
1579
+ if (typeof rootName === 'string' && transformCaseFunc(rootName) === 'template') {
1580
+ const content = root.content;
1581
+ if (_isDocumentFragment(content)) {
1582
+ _sanitizeAttachedShadowRoots2(content);
1583
+ }
1584
+ }
1585
+ }
1539
1586
  };
1540
1587
  // eslint-disable-next-line complexity
1541
1588
  DOMPurify.sanitize = function (dirty) {
@@ -1626,7 +1673,7 @@
1626
1673
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
1627
1674
  // eslint-disable-next-line unicorn/prefer-includes
1628
1675
  dirty.indexOf('<') === -1) {
1629
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
1676
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
1630
1677
  }
1631
1678
  /* Initialize the document to work on */
1632
1679
  body = _initDocument(dirty);
@@ -1658,14 +1705,14 @@
1658
1705
  /* If we sanitized `dirty` in-place, return it. */
1659
1706
  if (IN_PLACE) {
1660
1707
  if (SAFE_FOR_TEMPLATES) {
1661
- _scrubTemplateExpressions(dirty);
1708
+ _scrubTemplateExpressions2(dirty);
1662
1709
  }
1663
1710
  return dirty;
1664
1711
  }
1665
1712
  /* Return sanitized string or DOM */
1666
1713
  if (RETURN_DOM) {
1667
1714
  if (SAFE_FOR_TEMPLATES) {
1668
- _scrubTemplateExpressions(body);
1715
+ _scrubTemplateExpressions2(body);
1669
1716
  }
1670
1717
  if (RETURN_DOM_FRAGMENT) {
1671
1718
  returnNode = createDocumentFragment.call(body.ownerDocument);
@@ -1699,7 +1746,7 @@
1699
1746
  serializedHTML = stringReplace(serializedHTML, expr, ' ');
1700
1747
  });
1701
1748
  }
1702
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
1749
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
1703
1750
  };
1704
1751
  DOMPurify.setConfig = function () {
1705
1752
  let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};