dompurify 3.4.12 → 3.4.13

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
8
8
 
9
- It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.4.12**.
9
+ It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.4.13**.
10
10
 
11
11
  DOMPurify runs as JavaScript and works in all modern browsers (Safari (10+), Opera (15+), Edge, Firefox and Chrome - as well as almost anything else using Blink, Gecko or WebKit). It doesn't break on MSIE or other legacy browsers. It simply does nothing.
12
12
 
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.12 | (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.12/LICENSE */
1
+ /*! @license DOMPurify 3.4.13 | (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.13/LICENSE */
2
2
 
3
3
  import { TrustedTypePolicy, TrustedTypesWindow, TrustedHTML } from 'trusted-types/lib/index.js';
4
4
 
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.12 | (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.12/LICENSE */
1
+ /*! @license DOMPurify 3.4.13 | (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.13/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
@@ -426,7 +426,7 @@ const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options
426
426
  function createDOMPurify() {
427
427
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
428
428
  const DOMPurify = root => createDOMPurify(root);
429
- DOMPurify.version = '3.4.12';
429
+ DOMPurify.version = '3.4.13';
430
430
  DOMPurify.removed = [];
431
431
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
432
432
  // Not running in a browser, provide a factory function
@@ -457,6 +457,7 @@ function createDOMPurify() {
457
457
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
458
458
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
459
459
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
460
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
460
461
  // As per issue #47, the web-components registry is inherited by a
461
462
  // new document created via createHTMLDocument. As per the spec
462
463
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -1375,7 +1376,17 @@ function createDOMPurify() {
1375
1376
  * @return The created NodeIterator
1376
1377
  */
1377
1378
  const _createNodeIterator = function _createNodeIterator(root) {
1378
- return createNodeIterator.call(root.ownerDocument || root, root,
1379
+ /* Read ownerDocument through the cached Node.prototype getter, never the
1380
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
1381
+ clobbering child (<input name="ownerDocument"> or a form-associated
1382
+ external input) shadows the prototype getter and makes a direct read
1383
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
1384
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
1385
+ the walk's fail-closed barrier - leaving the caller's live tree, with
1386
+ any already-armed handler in it, un-neutralized. The cached getter
1387
+ returns the real Document regardless of the clobber. */
1388
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
1389
+ return createNodeIterator.call(doc || root, root,
1379
1390
  // eslint-disable-next-line no-bitwise
1380
1391
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1381
1392
  };
@@ -1415,7 +1426,11 @@ function createDOMPurify() {
1415
1426
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1416
1427
  var _node$querySelectorAl;
1417
1428
  node.normalize();
1418
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
1429
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
1430
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
1431
+ carry a form-named-getter override of ownerDocument. */
1432
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1433
+ const walker = createNodeIterator.call(doc || node, node,
1419
1434
  // eslint-disable-next-line no-bitwise
1420
1435
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1421
1436
  let currentNode = walker.nextNode();
@@ -1582,7 +1597,7 @@ function createDOMPurify() {
1582
1597
  * @param tagName the node's transformCaseFunc'd tag name
1583
1598
  * @return true if the node was removed, false if kept
1584
1599
  */
1585
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1600
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1586
1601
  /* Check if we have a custom element to handle */
1587
1602
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1588
1603
  if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
@@ -1605,27 +1620,26 @@ function createDOMPurify() {
1605
1620
  const childNodes = getChildNodes(currentNode);
1606
1621
  if (childNodes && parentNode) {
1607
1622
  const childCount = childNodes.length;
1608
- /* In-place: hoist the *original* children so the iterator visits
1609
- and sanitises them through the same allowlist pass as every other
1610
- node. The caller built the tree in the live document, so the
1611
- originals carry already-queued resource events (`<img onerror>`,
1612
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
1613
- those originals detached but still armed, firing in page scope
1614
- while the returned tree looked clean. Moving is safe in-place: the
1615
- root is pre-validated as an allowed tag and so is never the node
1616
- being removed, which keeps `parentNode` inside the iterator root
1617
- and the relocated child inside the serialised tree.
1618
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1619
- at and the result serialised from `body`, so a restrictive
1620
- ALLOWED_TAGS that removes `body` itself must leave its content in
1621
- place, which only cloning does; and those paths parse into an
1622
- inert document, so their discarded originals never had a queued
1623
- event to neutralise.
1623
+ /* Hoist by moving each child up one level rather than deep-cloning
1624
+ it. Moving transfers every descendant exactly once, so a chain of
1625
+ nested disallowed elements costs O(n) instead of the O(n^2) that
1626
+ re-cloning the shrinking subtree at each level produced; it also
1627
+ empties the removed original, so `DOMPurify.removed` no longer
1628
+ pins whole subtrees. Moving preserves the in-place guarantee too:
1629
+ an original carrying already-queued resource events (`<img
1630
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
1631
+ relocated and sanitised rather than left detached but still armed.
1632
+ The sole case that must clone is removing the walk root itself.
1633
+ The result is serialised from the root's subtree, so a restrictive
1634
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
1635
+ leave the content inside it, which only cloning does. In IN_PLACE
1636
+ the root is pre-validated as an allowed tag and so is never removed
1637
+ here, so that path always takes the move branch.
1624
1638
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1625
1639
  valid whether we move (drops the trailing entry) or clone (leaves
1626
1640
  the list intact). */
1627
1641
  for (let i = childCount - 1; i >= 0; --i) {
1628
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1642
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
1629
1643
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1630
1644
  }
1631
1645
  }
@@ -1633,6 +1647,26 @@ function createDOMPurify() {
1633
1647
  _forceRemove(currentNode);
1634
1648
  return true;
1635
1649
  };
1650
+ /**
1651
+ * Fork a hook-mutable allowlist off its shared binding the first time a
1652
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
1653
+ * hook cannot widen the per-instance default or the setConfig binding by
1654
+ * reference and leak past the call. Returns the set unchanged once it is
1655
+ * already call-local, so repeated calls across elements are idempotent.
1656
+ *
1657
+ * @param hookList the uponSanitize* hook array for this event
1658
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
1659
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
1660
+ * @param setConfigSet the captured setConfig() binding, or null
1661
+ * @return a call-local clone if a hook is present and set is still shared,
1662
+ * else set unchanged
1663
+ */
1664
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
1665
+ if (hookList.length === 0) {
1666
+ return set;
1667
+ }
1668
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
1669
+ };
1636
1670
  /**
1637
1671
  * _sanitizeElements
1638
1672
  *
@@ -1646,9 +1680,14 @@ function createDOMPurify() {
1646
1680
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1647
1681
  /* Execute a hook if present */
1648
1682
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1649
- /* A hook may have detached the node treat it as removed (see the
1650
- detached-node comment after the uponSanitizeElement hook below). */
1683
+ /* A hook may have detached the node - treat it as removed (see the
1684
+ detached-node comment after the uponSanitizeElement hook below). On
1685
+ the IN_PLACE path, neutralize the detached subtree first so a queued
1686
+ resource handler on it cannot fire in page scope after we return. */
1651
1687
  if (currentNode !== root && getParentNode(currentNode) === null) {
1688
+ if (IN_PLACE) {
1689
+ _neutralizeSubtree(currentNode);
1690
+ }
1652
1691
  return true;
1653
1692
  }
1654
1693
  /* Check if element is clobbered or can clobber */
@@ -1658,6 +1697,12 @@ function createDOMPurify() {
1658
1697
  }
1659
1698
  /* Now let's check the element's type and name */
1660
1699
  const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1700
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
1701
+ hook may have been installed after that guard sampled the hook arrays
1702
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
1703
+ aliasing a shared binding that a widening hook would mutate by
1704
+ reference. Fork it before exposing it to the hook. */
1705
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
1661
1706
  /* Execute a hook if present */
1662
1707
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1663
1708
  tagName,
@@ -1675,10 +1720,22 @@ function createDOMPurify() {
1675
1720
  opposite of a node that is already safely gone. The walk root is
1676
1721
  exempt: a detached IN_PLACE root is legitimate input and must still
1677
1722
  be fully sanitized, and a kill-decision on it must keep hitting the
1678
- REPORT-3 throw. Nodes detached by hooks are the hook's
1679
- responsibility: they are not recorded in DOMPurify.removed and are
1680
- not neutralized by the post-walk IN_PLACE pass. */
1723
+ REPORT-3 throw. Nodes detached by hooks stay the hook's
1724
+ responsibility for placement: they are not recorded in
1725
+ DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
1726
+ DOMPurify.removed) does not reach them. But a hook-detached subtree
1727
+ can still hold a queued resource-event handler - e.g. an <img onload>
1728
+ that began loading when the caller built the live tree - which fires
1729
+ in page scope after sanitize returns even though the handler never
1730
+ reached the returned tree. That is the audit-5 F1 hazard, and the
1731
+ documented node.remove() hook pattern walks straight into it. So on
1732
+ the IN_PLACE path we neutralize the detached subtree inline here,
1733
+ stripping its non-allow-listed attributes before returning, exactly
1734
+ as the post-walk pass does for _forceRemove'd subtrees. */
1681
1735
  if (currentNode !== root && getParentNode(currentNode) === null) {
1736
+ if (IN_PLACE) {
1737
+ _neutralizeSubtree(currentNode);
1738
+ }
1682
1739
  return true;
1683
1740
  }
1684
1741
  /* Remove mXSS vectors, processing instructions and risky comments */
@@ -1688,7 +1745,7 @@ function createDOMPurify() {
1688
1745
  }
1689
1746
  /* Remove element if anything forbids its presence */
1690
1747
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1691
- const removed = _sanitizeDisallowedNode(currentNode, tagName);
1748
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
1692
1749
  /* A false return means the node is a custom element kept via
1693
1750
  CUSTOM_ELEMENT_HANDLING - the only keep path through
1694
1751
  _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
@@ -1893,6 +1950,9 @@ function createDOMPurify() {
1893
1950
  if (!attributes || _isClobbered(currentNode)) {
1894
1951
  return;
1895
1952
  }
1953
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
1954
+ attribute allowlist off its shared binding before a hook can see it. */
1955
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
1896
1956
  const hookEvent = {
1897
1957
  attrName: '',
1898
1958
  attrValue: '',
@@ -2265,17 +2325,20 @@ function createDOMPurify() {
2265
2325
  }
2266
2326
  /* Get node iterator */
2267
2327
  const walkRoot = inPlace ? dirty : body;
2268
- const nodeIterator = _createNodeIterator(walkRoot);
2269
2328
  /* Now start iterating over the created document.
2270
2329
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2271
2330
  engine/custom-element mutation can detach a node mid-walk so
2272
2331
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2273
2332
  barrier the caller's in-place tree would be left half-sanitized with the
2274
- unvisited tail still armed. On any throw we fail closed — strip the
2275
- in-place root bare then rethrow so the existing throw contract is
2276
- preserved. (String/DOM-copy paths never return the partial body, so the
2277
- propagating throw is already fail-closed there.) */
2333
+ unvisited tail still armed. _createNodeIterator itself is inside the
2334
+ barrier too: constructing the iterator dereferences the root's document,
2335
+ and any failure there (e.g. an exotic/clobbered root) must still fail
2336
+ closed rather than skip the neutralize. On any throw we fail closed -
2337
+ strip the in-place root bare - then rethrow so the existing throw
2338
+ contract is preserved. (String/DOM-copy paths never return the partial
2339
+ body, so the propagating throw is already fail-closed there.) */
2278
2340
  try {
2341
+ const nodeIterator = _createNodeIterator(walkRoot);
2279
2342
  while (currentNode = nodeIterator.nextNode()) {
2280
2343
  /* Sanitize tags and elements */
2281
2344
  _sanitizeElements(currentNode, walkRoot);