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