@vaadin/bundles 24.10.9 → 24.10.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.
@@ -10,7 +10,7 @@ __webpack_require__.r(__webpack_exports__);
10
10
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
11
11
  /* harmony export */ "default": () => (/* binding */ purify_default)
12
12
  /* harmony export */ });
13
- /*! @license DOMPurify 3.4.16 | (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.16/LICENSE */
13
+ /*! @license DOMPurify 3.4.17 | (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.17/LICENSE */
14
14
  function _OverloadYield(e, d) {
15
15
  this.v = e, this.k = d;
16
16
  }
@@ -134,9 +134,12 @@ Array.prototype.indexOf;
134
134
  const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
135
135
  const arrayPop = unapply(Array.prototype.pop);
136
136
  const arrayPush = unapply(Array.prototype.push);
137
- Array.prototype.slice;
137
+ const arraySlice = unapply(Array.prototype.slice);
138
138
  const arraySplice = unapply(Array.prototype.splice);
139
139
  const arrayIsArray = Array.isArray;
140
+ const setCreate = unconstruct(Set);
141
+ const setAdd = unapply(Set.prototype.add);
142
+ const setHas = unapply(Set.prototype.has);
140
143
  const stringToLowerCase = unapply(String.prototype.toLowerCase);
141
144
  const stringToString = unapply(String.prototype.toString);
142
145
  const stringMatch = unapply(String.prototype.match);
@@ -281,6 +284,25 @@ function lookupGetter(object, prop) {
281
284
  }
282
285
  return fallbackValue;
283
286
  }
287
+ /**
288
+ * Like lookupGetter, but for accessor properties only, and returns null
289
+ * instead of a null-returning fallback when no getter exists on the chain.
290
+ * Lets a caller fall back to a direct property read in environments that
291
+ * lack the getter, where a fallback returning null would be indistinguishable
292
+ * from a legitimate null value (e.g. namespaceURI of a no-namespace element).
293
+ *
294
+ * @param object - The object (usually a prototype) to start the lookup at.
295
+ * @param prop - The accessor property name.
296
+ * @returns The unapplied getter, or null if none was found.
297
+ */
298
+ function lookupAccessor(object, prop) {
299
+ while (object !== null) {
300
+ const desc = getOwnPropertyDescriptor(object, prop);
301
+ if (desc) return desc.get ? unapply(desc.get) : null;
302
+ object = getPrototypeOf(object);
303
+ }
304
+ return null;
305
+ }
284
306
  function isRegex(value) {
285
307
  try {
286
308
  regExpTest(value, "");
@@ -965,7 +987,7 @@ const NODE_TYPE = {
965
987
  documentFragment: 11,
966
988
  notation: 12
967
989
  };
968
- const LITERAL_TEXT_ELEMENT_NAMES = [
990
+ const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
969
991
  "style",
970
992
  "script",
971
993
  "xmp",
@@ -974,15 +996,7 @@ const LITERAL_TEXT_ELEMENT_NAMES = [
974
996
  "noframes",
975
997
  "plaintext",
976
998
  "noscript"
977
- ];
978
- const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
979
- const LITERAL_TEXT_CLOSE = function() {
980
- const map = {};
981
- arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
982
- map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
983
- });
984
- return freeze(map);
985
- }();
999
+ ]));
986
1000
  const getGlobal = function getGlobal() {
987
1001
  return typeof window === "undefined" ? null : window;
988
1002
  };
@@ -1060,7 +1074,7 @@ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallbac
1060
1074
  function createDOMPurify() {
1061
1075
  let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
1062
1076
  const DOMPurify = (root) => createDOMPurify(root);
1063
- DOMPurify.version = "3.4.16";
1077
+ DOMPurify.version = "3.4.17";
1064
1078
  DOMPurify.removed = [];
1065
1079
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
1066
1080
  DOMPurify.isSupported = false;
@@ -1092,6 +1106,10 @@ function createDOMPurify() {
1092
1106
  const _readNodeName = function _readNodeName(node) {
1093
1107
  return getNodeName ? getNodeName(node) : node.nodeName;
1094
1108
  };
1109
+ const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
1110
+ const _readNamespaceURI = function _readNamespaceURI(element) {
1111
+ return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
1112
+ };
1095
1113
  if (typeof HTMLTemplateElement === "function") {
1096
1114
  const template = document.createElement("template");
1097
1115
  if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
@@ -1212,6 +1230,7 @@ function createDOMPurify() {
1212
1230
  const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
1213
1231
  let KEEP_CONTENT = true;
1214
1232
  let IN_PLACE = false;
1233
+ let IN_PLACE_VISITED = null;
1215
1234
  let USE_PROFILES = {};
1216
1235
  let FORBID_CONTENTS = null;
1217
1236
  const DEFAULT_FORBID_CONTENTS = addToSet({}, [
@@ -1485,18 +1504,23 @@ function createDOMPurify() {
1485
1504
  * return. Return true otherwise.
1486
1505
  */
1487
1506
  const _checkValidNamespace = function _checkValidNamespace(element) {
1488
- let parent = getParentNode(element);
1489
- if (!parent || !parent.tagName) parent = {
1490
- namespaceURI: NAMESPACE,
1491
- tagName: "template"
1492
- };
1493
- const tagName = stringToLowerCase(element.tagName);
1494
- const parentTagName = stringToLowerCase(parent.tagName);
1495
- if (!ALLOWED_NAMESPACES[element.namespaceURI]) return false;
1496
- if (element.namespaceURI === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1497
- if (element.namespaceURI === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1498
- if (element.namespaceURI === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1499
- if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) return true;
1507
+ const elementNamespace = _readNamespaceURI(element);
1508
+ const tagName = stringToLowerCase(_readNodeName(element));
1509
+ const parentNode = getParentNode(element);
1510
+ let parent;
1511
+ let parentTagName;
1512
+ if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
1513
+ parent = { namespaceURI: _readNamespaceURI(parentNode) };
1514
+ parentTagName = stringToLowerCase(_readNodeName(parentNode));
1515
+ } else {
1516
+ parent = { namespaceURI: NAMESPACE };
1517
+ parentTagName = "template";
1518
+ }
1519
+ if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
1520
+ if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
1521
+ if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
1522
+ if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
1523
+ if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
1500
1524
  return false;
1501
1525
  };
1502
1526
  /**
@@ -1540,36 +1564,21 @@ function createDOMPurify() {
1540
1564
  }
1541
1565
  };
1542
1566
  /**
1543
- * _neutralizeRoot
1544
- *
1545
- * Fail-closed teardown of an in-place root after the sanitize walk aborts
1546
- * (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
1547
- * custom element's reaction detaches a node so `_forceRemove`'s deliberate
1548
- * parentless guard throws, or any other re-entrant engine mutation — would
1549
- * otherwise leave the caller's *live* tree half-sanitized, with everything
1550
- * after the abort point still carrying its handlers. There is no safe way
1551
- * to resume the walk (the tree mutated under us), so we strip the root bare:
1552
- * remove every child and every attribute, then let the caller's catch see
1553
- * the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
1554
- * getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
1567
+ * Fail-closed teardown of an in-place root: strip every disallowed attribute
1568
+ * in the subtree first, so nothing armed is left behind, then remove all
1569
+ * children and the root's own attributes.
1555
1570
  *
1556
1571
  * @param root the in-place root to empty
1557
1572
  */
1558
1573
  const _neutralizeRoot = function _neutralizeRoot(root) {
1559
1574
  _neutralizeSubtree(root);
1560
1575
  const childNodes = getChildNodes(root);
1561
- if (childNodes) {
1562
- const snapshot = [];
1563
- arrayForEach(childNodes, (child) => {
1564
- arrayPush(snapshot, child);
1565
- });
1566
- arrayForEach(snapshot, (child) => {
1567
- try {
1568
- remove(child);
1569
- } catch (_) {}
1570
- });
1571
- }
1572
- const attributes = getAttributes(root);
1576
+ if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
1577
+ try {
1578
+ remove(child);
1579
+ } catch (_) {}
1580
+ });
1581
+ const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
1573
1582
  if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
1574
1583
  const attribute = attributes[i];
1575
1584
  const name = attribute && attribute.name;
@@ -1577,18 +1586,9 @@ function createDOMPurify() {
1577
1586
  }
1578
1587
  };
1579
1588
  /**
1580
- * _removeAttribute
1581
- *
1582
- * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1583
- * lookup key for HTML elements in an HTML document, so they silently miss an
1584
- * attribute whose stored qualified name still contains uppercase ASCII
1585
- * letters. That happens when the node came from a case-preserving source
1586
- * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1587
- * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1588
- * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1589
- * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1590
- * case- and namespace-exact, and fall back to name-based removal only when
1591
- * the caller could not supply the node.
1589
+ * Removes an attribute, preferably as its exact Attr node: name-based
1590
+ * removal lowercases the name in HTML documents and misses case-preserved
1591
+ * names, e.g. from imported XHTML.
1592
1592
  *
1593
1593
  * @param name an Attribute name
1594
1594
  * @param element a DOM node
@@ -1622,12 +1622,8 @@ function createDOMPurify() {
1622
1622
  }
1623
1623
  };
1624
1624
  /**
1625
- * _stripDisallowedAttributes
1626
- *
1627
- * Removes every attribute the active configuration does not allow from a
1628
- * single element, using the same allowlist as the main attribute pass (so
1629
- * `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
1630
- * neutralise nodes that are being discarded from an in-place tree.
1625
+ * Removes every attribute the configuration does not allow. Used to
1626
+ * neutralize nodes discarded from an in-place tree.
1631
1627
  *
1632
1628
  * @param element the element to strip
1633
1629
  */
@@ -1642,77 +1638,158 @@ function createDOMPurify() {
1642
1638
  }
1643
1639
  };
1644
1640
  /**
1645
- * _neutralizeSubtree
1641
+ * Iterative depth-first walk, so deep trees cannot overflow the call stack,
1642
+ * reading through the cached getters, so clobbering cannot hide children.
1643
+ * When `visit` returns false, the node's children are skipped. With
1644
+ * `intoShadow`, open shadow roots are walked after their host's children.
1646
1645
  *
1647
- * Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
1648
- * move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
1649
- * namespace, comment, processing-instruction and KEEP_CONTENT:false removals
1650
- * all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
1651
- * those dropped nodes are detached from the caller's LIVE tree but a
1652
- * handler-bearing original among them (an `<img onerror>`/`<video>` that was
1653
- * loading) keeps its queued resource event, which fires in page scope after
1654
- * sanitize returns. This walks a removed subtree and strips every attribute
1655
- * the active configuration does not allow — so `on*` handlers are cancelled
1656
- * through the SAME allowlist that governs kept nodes, not a separate `/^on/`
1657
- * blocklist. Run synchronously before sanitize returns, i.e. before any
1658
- * queued event can fire. Hook-free by design: these nodes leave the output,
1659
- * so firing attribute hooks for them would be surprising. Clobber-safe reads;
1660
- * a doomed clobbered node may shadow `removeAttribute` (its own attributes are
1661
- * irrelevant — it is discarded — while its non-clobbered descendants, e.g.
1662
- * the `<img>`, are reached and scrubbed).
1663
- *
1664
- * @param root the root of a removed subtree to neutralise
1646
+ * @param root where to start
1647
+ * @param visit called for every node
1648
+ * @param intoShadow whether to enter open shadow roots
1665
1649
  */
1666
- const _neutralizeSubtree = function _neutralizeSubtree(root) {
1650
+ const _walkTree = function _walkTree(root, visit) {
1651
+ let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
1667
1652
  const stack = [root];
1668
1653
  while (stack.length > 0) {
1669
1654
  const node = stack.pop();
1670
- if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1655
+ if (visit(node) === false) continue;
1656
+ if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
1657
+ const shadowRoot = getShadowRoot(node);
1658
+ if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
1659
+ }
1671
1660
  const childNodes = getChildNodes(node);
1672
1661
  if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1673
1662
  }
1674
1663
  };
1675
1664
  /**
1676
- * _neutralizePatchLinkage
1665
+ * Strips disallowed attributes from every element of a discarded subtree,
1666
+ * so a handler that is already queued (a loading image) cannot fire.
1677
1667
  *
1678
- * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1679
- * hardening, https://github.com/WICG/declarative-partial-updates).
1668
+ * @param root the root of a removed subtree
1669
+ */
1670
+ const _neutralizeSubtree = function _neutralizeSubtree(root) {
1671
+ _walkTree(root, (node) => {
1672
+ if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
1673
+ });
1674
+ };
1675
+ /**
1676
+ * All nodes under `root`, root first, including open shadow roots. Template
1677
+ * content is not entered, it is inert.
1680
1678
  *
1681
- * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1682
- * markers (PIs / markup comments) node-by-node, in document order, AS it
1683
- * reaches each node. On a live in-place root that leaves a window: from the
1684
- * moment the root is connected until the walk arrives at a given node, that
1685
- * node's linkage is live. A patch applied on connection/stream can fire as
1686
- * a microtask during the walk and inject or teleport an unsanitized DOM
1687
- * range into a region the iterator has already passed and will not revisit,
1688
- * so the post-return "tree is sanitized" contract is violated. Sweep the
1689
- * whole tree once up front and sever every linkage before the walk begins,
1690
- * closing that window.
1679
+ * @param root the in-place root
1680
+ * @return the nodes
1681
+ */
1682
+ const _collectLiveNodes = function _collectLiveNodes(root) {
1683
+ const nodes = [];
1684
+ _walkTree(root, (node) => {
1685
+ nodes.push(node);
1686
+ }, true);
1687
+ return nodes;
1688
+ };
1689
+ /**
1690
+ * Strips disallowed attributes from every element that was under the
1691
+ * in-place root when the call started and no longer is, whatever removed
1692
+ * or moved it.
1691
1693
  *
1692
- * This CANNOT undo a patch that already fired before sanitize ran — that is
1693
- * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1694
- * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1695
- * to group with the rest of the declarative-partial-updates handling and
1696
- * stay overridable, consistent with the codebase.
1694
+ * @param root the in-place root
1695
+ * @param before the root's nodes, collected before the walk
1696
+ */
1697
+ const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
1698
+ const reachable = setCreate();
1699
+ arrayForEach(_collectLiveNodes(root), (node) => {
1700
+ if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
1701
+ });
1702
+ arrayForEach(before, (node) => {
1703
+ if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
1704
+ });
1705
+ };
1706
+ /**
1707
+ * Neutralizes everything that left the in-place tree during the call: what
1708
+ * DOMPurify removed, and what hooks or custom elements removed or moved out.
1697
1709
  *
1698
- * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1699
- * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1710
+ * @param root the in-place root
1711
+ * @param before the root's nodes, collected before the walk
1712
+ * @return true if the root itself was removed
1713
+ */
1714
+ const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
1715
+ let rootRemoved = false;
1716
+ arrayForEach(DOMPurify.removed, (entry) => {
1717
+ if (entry.element) {
1718
+ rootRemoved = rootRemoved || entry.element === root;
1719
+ _neutralizeSubtree(entry.element);
1720
+ }
1721
+ });
1722
+ _neutralizeEscapedElements(root, before);
1723
+ return rootRemoved;
1724
+ };
1725
+ const MAX_UNVISITED_ROUNDS = 8;
1726
+ /**
1727
+ * The walk only moves forward, so a node moved to a position it already
1728
+ * passed is never visited. Give every original node that is still in the
1729
+ * tree but unvisited the normal treatment, hooks included. Nodes created
1730
+ * during the call are left alone. Fails closed if the tree keeps changing.
1700
1731
  *
1701
- * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1702
- * .html Q1): this mirrors the existing policy of keeping `for` on
1703
- * <label>/<output>. If the shipping feature can drive a patch through a
1704
- * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1705
- * attribute check at _isBasicCustomElement's caller must additionally drop
1706
- * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1732
+ * @param root the in-place root
1733
+ * @param before the root's nodes, collected before the walk
1734
+ */
1735
+ const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
1736
+ const visited = IN_PLACE_VISITED;
1737
+ let original = null;
1738
+ const getOriginal = () => {
1739
+ if (!original) {
1740
+ original = setCreate();
1741
+ arrayForEach(before, (beforeNode) => {
1742
+ setAdd(original, beforeNode);
1743
+ });
1744
+ }
1745
+ return original;
1746
+ };
1747
+ for (let round = 0;; round++) {
1748
+ const live = _collectLiveNodes(root);
1749
+ const containers = [root];
1750
+ let pending = false;
1751
+ arrayForEach(live, (node) => {
1752
+ if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
1753
+ else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
1754
+ });
1755
+ if (!pending) return;
1756
+ if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
1757
+ arrayForEach(containers, (container, index) => {
1758
+ _sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
1759
+ });
1760
+ }
1761
+ };
1762
+ /**
1763
+ * One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
1707
1764
  *
1708
- * @param root the in-place root to sweep
1765
+ * @param container the root or an open shadow root
1766
+ * @param inShadow whether container is a shadow root
1767
+ * @param root the in-place root
1768
+ * @param original the root's nodes collected before the walk, as a set
1709
1769
  */
1770
+ const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
1771
+ const iterator = _createNodeIterator(container);
1772
+ let node;
1773
+ while (node = iterator.nextNode()) {
1774
+ if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
1775
+ if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
1776
+ _sanitizeElements(node, root);
1777
+ _sanitizeAttributes(node, root);
1778
+ if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
1779
+ }
1780
+ };
1710
1781
  /**
1711
- * Central policy for declarative-partial-updates patch-linkage attributes,
1712
- * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1713
- * the two sites cannot drift: `patchsrc` always links, `for` links
1714
- * everywhere except on <label>/<output>, and the whole policy is gated on
1715
- * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1782
+ * Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
1783
+ *
1784
+ * @param node the node being processed
1785
+ */
1786
+ const _markVisited = function _markVisited(node) {
1787
+ if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
1788
+ };
1789
+ /**
1790
+ * Patch-linkage attributes (declarative partial updates): `patchsrc`
1791
+ * always, `for` everywhere except on <label> and <output>. Gated on
1792
+ * SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
1716
1793
  *
1717
1794
  * @param lcName the transformCaseFunc'd attribute name
1718
1795
  * @param lcTag the transformCaseFunc'd tag name of the carrying element
@@ -1723,17 +1800,26 @@ function createDOMPurify() {
1723
1800
  if (lcName === "patchsrc") return true;
1724
1801
  return lcName === "for" && lcTag !== "label" && lcTag !== "output";
1725
1802
  };
1803
+ /**
1804
+ * IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
1805
+ * can fire mid-walk into a part of the tree the walker already passed.
1806
+ * Removes range markers (processing instructions, comments with markup)
1807
+ * and strips linkage attributes. It cannot undo a patch that already fired.
1808
+ *
1809
+ * TODO: if the shipped feature can patch through `for` on <label>/<output>,
1810
+ * drop that pair here too (see test/declarative-patch-probe.html).
1811
+ *
1812
+ * @param root the in-place root to sweep
1813
+ */
1726
1814
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1727
1815
  if (!SAFE_FOR_XML) return;
1728
- const stack = [root];
1729
- while (stack.length > 0) {
1730
- const node = stack.pop();
1816
+ _walkTree(root, (node) => {
1731
1817
  const nodeType = _readNodeType(node);
1732
1818
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
1733
1819
  try {
1734
1820
  remove(node);
1735
1821
  } catch (_) {}
1736
- continue;
1822
+ return false;
1737
1823
  }
1738
1824
  if (nodeType === NODE_TYPE.element) {
1739
1825
  const element = node;
@@ -1743,9 +1829,8 @@ function createDOMPurify() {
1743
1829
  if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
1744
1830
  } catch (_) {}
1745
1831
  }
1746
- const childNodes = getChildNodes(node);
1747
- if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
1748
- }
1832
+ return true;
1833
+ });
1749
1834
  };
1750
1835
  /**
1751
1836
  * _initDocument
@@ -1822,6 +1907,27 @@ function createDOMPurify() {
1822
1907
  */
1823
1908
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1824
1909
  var _node$querySelectorAl;
1910
+ _scrubOwnCharacterData(node);
1911
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1912
+ if (templates) arrayForEach(templates, (tmpl) => {
1913
+ if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1914
+ });
1915
+ };
1916
+ /**
1917
+ * _scrubOwnCharacterData
1918
+ *
1919
+ * Non-recursive core of _scrubTemplateExpressions: normalize `node` and
1920
+ * strip template expressions from its own character data only. Does not
1921
+ * enter <template>.content or attached shadow roots (NodeIterator never
1922
+ * does). Used by _sanitizeShadowDOM, where every nested template content
1923
+ * and shadow root is already walked - and therefore scrubbed - by its own
1924
+ * recursive _sanitizeShadowDOM call; recursing here as well would rescrub
1925
+ * each nested fragment once per ancestor, which is quadratic in nesting
1926
+ * depth.
1927
+ *
1928
+ * @param node The root whose own character data should be scrubbed.
1929
+ */
1930
+ const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
1825
1931
  node.normalize();
1826
1932
  const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1827
1933
  const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
@@ -1830,10 +1936,6 @@ function createDOMPurify() {
1830
1936
  currentNode.data = _stripTemplateExpressions(currentNode.data);
1831
1937
  currentNode = walker.nextNode();
1832
1938
  }
1833
- const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
1834
- if (templates) arrayForEach(templates, (tmpl) => {
1835
- if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
1836
- });
1837
1939
  };
1838
1940
  /**
1839
1941
  * _isClobbered
@@ -1909,7 +2011,7 @@ function createDOMPurify() {
1909
2011
  */
1910
2012
  const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
1911
2013
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
1912
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) return true;
2014
+ if (SAFE_FOR_XML && LITERAL_TEXT_ELEMENTS[tagName] && _readNodeType(currentNode) === NODE_TYPE.element && _readNamespaceURI(currentNode) === HTML_NAMESPACE && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent))) return true;
1913
2015
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
1914
2016
  if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
1915
2017
  return false;
@@ -1953,7 +2055,7 @@ function createDOMPurify() {
1953
2055
  */
1954
2056
  const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1955
2057
  if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
1956
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
2058
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
1957
2059
  const parentNode = getParentNode(currentNode);
1958
2060
  const childNodes = getChildNodes(currentNode);
1959
2061
  if (childNodes && parentNode) {
@@ -1986,35 +2088,13 @@ function createDOMPurify() {
1986
2088
  return set === defaultSet || set === setConfigSet ? clone(set) : set;
1987
2089
  };
1988
2090
  /**
1989
- * Shared guard for a node that a hook has detached from the walk tree,
1990
- * used after each element-hook site in _sanitizeElements. Detaching is a
1991
- * long-standing user pattern (issue #469; draw.io-style foreignObject
1992
- * filtering). Per the cached, unclobberable parentNode getter the node is
1993
- * genuinely out of the tree, so it can reach neither the serialized
1994
- * output nor an IN_PLACE live tree; treat it as removed and stop
1995
- * processing it. Without this guard, the unsafe-node / namespace checks
1996
- * would call _forceRemove on a parentless node and hit the REPORT-3
1997
- * fail-closed throw — which exists for nodes DOMPurify wants gone but
1998
- * *cannot* detach (clobbered / parentless roots), the opposite of a node
1999
- * that is already safely gone. The walk root is exempt: a detached
2000
- * IN_PLACE root is legitimate input and must still be fully sanitized,
2001
- * and a kill-decision on it must keep hitting the REPORT-3 throw.
2002
- *
2003
- * Nodes detached by hooks stay the hook's responsibility for placement:
2004
- * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
2005
- * pass (which iterates DOMPurify.removed) does not reach them. But a
2006
- * hook-detached subtree can still hold a queued resource-event handler -
2007
- * e.g. an <img onload> that began loading when the caller built the live
2008
- * tree - which fires in page scope after sanitize returns even though the
2009
- * handler never reached the returned tree. That is the audit-5 F1 hazard,
2010
- * and the documented node.remove() hook pattern walks straight into it.
2011
- * So on the IN_PLACE path we neutralize the detached subtree inline,
2012
- * stripping its non-allow-listed attributes before returning, exactly as
2013
- * the post-walk pass does for _forceRemove'd subtrees.
2091
+ * A hook may have detached the node it was called for. If so, treat it as
2092
+ * removed and stop processing it; the walk root is exempt. On the IN_PLACE
2093
+ * path the detached subtree is neutralized right away.
2014
2094
  *
2015
2095
  * @param currentNode the node a hook may have detached
2016
2096
  * @param root the current walk root
2017
- * @return true if the node is detached and now handled, false otherwise
2097
+ * @return true if the node was detached
2018
2098
  */
2019
2099
  const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
2020
2100
  if (currentNode === root || getParentNode(currentNode) !== null) return false;
@@ -2031,6 +2111,7 @@ function createDOMPurify() {
2031
2111
  * @return true if node was killed, false if left alive
2032
2112
  */
2033
2113
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
2114
+ _markVisited(currentNode);
2034
2115
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
2035
2116
  if (_handleHookDetachedNode(currentNode, root)) return true;
2036
2117
  if (_isClobbered(currentNode)) {
@@ -2174,6 +2255,22 @@ function createDOMPurify() {
2174
2255
  }
2175
2256
  };
2176
2257
  /**
2258
+ * _isUnsafeAttributeValue
2259
+ *
2260
+ * Value-based attribute checks that the attribute walk applies before any
2261
+ * hook can force-keep an attribute, shared with the public
2262
+ * isValidAttribute() so that it never approves a value sanitize() would
2263
+ * strip.
2264
+ *
2265
+ * @param name the attribute name as found on the node (case preserved)
2266
+ * @param value the attribute value
2267
+ * @returns true if the attribute must be removed because of its value
2268
+ */
2269
+ const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
2270
+ if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
2271
+ return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
2272
+ };
2273
+ /**
2177
2274
  * _sanitizeAttributes
2178
2275
  *
2179
2276
  * @protect attributes
@@ -2217,11 +2314,7 @@ function createDOMPurify() {
2217
2314
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
2218
2315
  recreatedNamedProp = true;
2219
2316
  }
2220
- if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
2221
- _removeAttribute(name, currentNode, attr);
2222
- continue;
2223
- }
2224
- if (lcName === "attributename" && stringMatch(value, "href")) {
2317
+ if (_isUnsafeAttributeValue(name, value)) {
2225
2318
  _removeAttribute(name, currentNode, attr);
2226
2319
  continue;
2227
2320
  }
@@ -2269,24 +2362,13 @@ function createDOMPurify() {
2269
2362
  }
2270
2363
  }
2271
2364
  }
2365
+ if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
2272
2366
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
2273
2367
  };
2274
2368
  /**
2275
- * _sanitizeAttachedShadowRoots
2276
- *
2277
- * Walks `root` and feeds every attached shadow root we encounter into
2278
- * the existing _sanitizeShadowDOM pipeline. The default node iterator
2279
- * does not descend into shadow trees, so nodes inside an attached
2280
- * shadow root would otherwise be skipped entirely.
2281
- *
2282
- * Two real input paths put attached shadow roots in front of us:
2283
- * 1. IN_PLACE on a DOM node that already has shadow roots attached.
2284
- * 2. DOM-node input where importNode(dirty, true) deep-clones the
2285
- * shadow root because it was created with `clonable: true`.
2286
- *
2287
- * This pass runs once, up front, so the main iteration loop (and the
2288
- * existing _sanitizeShadowDOM template-content recursion) stay
2289
- * untouched — string-input paths are not affected.
2369
+ * Sanitizes every attached shadow root under `root`, since the node
2370
+ * iterator does not enter shadow trees. Needed for IN_PLACE input and for
2371
+ * DOM input whose clonable shadow roots were copied by importNode().
2290
2372
  *
2291
2373
  * @param root the subtree root to walk for attached shadow roots
2292
2374
  */
@@ -2351,6 +2433,8 @@ function createDOMPurify() {
2351
2433
  if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
2352
2434
  DOMPurify.removed = [];
2353
2435
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
2436
+ let inPlaceNodes = [];
2437
+ const outerVisited = IN_PLACE_VISITED;
2354
2438
  if (inPlace) {
2355
2439
  _neutralizePatchLinkage(dirty);
2356
2440
  const nn = _readNodeName(dirty);
@@ -2365,12 +2449,8 @@ function createDOMPurify() {
2365
2449
  _neutralizeRoot(dirty);
2366
2450
  throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
2367
2451
  }
2368
- try {
2369
- _sanitizeAttachedShadowRoots(dirty);
2370
- } catch (error) {
2371
- _neutralizeRoot(dirty);
2372
- throw error;
2373
- }
2452
+ inPlaceNodes = _collectLiveNodes(dirty);
2453
+ IN_PLACE_VISITED = setCreate();
2374
2454
  } else if (_isNode(dirty)) {
2375
2455
  body = _initDocument("<!---->");
2376
2456
  importedNode = body.ownerDocument.importNode(dirty, true);
@@ -2386,30 +2466,25 @@ function createDOMPurify() {
2386
2466
  if (body && FORCE_BODY) _forceRemove(body.firstChild);
2387
2467
  const walkRoot = inPlace ? dirty : body;
2388
2468
  try {
2469
+ if (inPlace) _sanitizeAttachedShadowRoots(dirty);
2389
2470
  const nodeIterator = _createNodeIterator(walkRoot);
2390
2471
  while (currentNode = nodeIterator.nextNode()) {
2391
2472
  _sanitizeElements(currentNode, walkRoot);
2392
2473
  _sanitizeAttributes(currentNode, walkRoot);
2393
2474
  if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
2394
2475
  }
2476
+ if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
2395
2477
  } catch (error) {
2396
2478
  if (inPlace) {
2397
2479
  _neutralizeRoot(dirty);
2398
- arrayForEach(DOMPurify.removed, (entry) => {
2399
- if (entry.element) _neutralizeSubtree(entry.element);
2400
- });
2480
+ _neutralizeDiscarded(dirty, inPlaceNodes);
2401
2481
  }
2402
2482
  throw error;
2483
+ } finally {
2484
+ IN_PLACE_VISITED = outerVisited;
2403
2485
  }
2404
2486
  if (inPlace) {
2405
- let rootWasRemoved = false;
2406
- arrayForEach(DOMPurify.removed, (entry) => {
2407
- if (entry.element) {
2408
- if (entry.element === dirty) rootWasRemoved = true;
2409
- _neutralizeSubtree(entry.element);
2410
- }
2411
- });
2412
- if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2487
+ if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
2413
2488
  if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
2414
2489
  return dirty;
2415
2490
  }
@@ -2446,6 +2521,7 @@ function createDOMPurify() {
2446
2521
  if (!CONFIG) _parseConfig({});
2447
2522
  const lcTag = transformCaseFunc(tag);
2448
2523
  const lcName = transformCaseFunc(attr);
2524
+ if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
2449
2525
  return _isValidAttribute(lcTag, lcName, value);
2450
2526
  };
2451
2527
  DOMPurify.addHook = function(entryPoint, hookFunction) {