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