@vaadin/bundles 24.9.20 → 24.9.22
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/.github/workflows/validation.yml +1 -1
- package/package.json +140 -140
- package/vaadin-bundle.json +1438 -1438
- package/vaadin.js +1195 -1102
- package/vaadin.js.map +1 -1
- package/vendors-node_modules_dompurify_dist_purify_es_mjs.js +272 -196
- package/vendors-node_modules_dompurify_dist_purify_es_mjs.js.map +1 -1
- package/vendors-node_modules_dompurify_dist_purify_js.js +272 -196
- package/vendors-node_modules_dompurify_dist_purify_js.js.map +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
\***********************************************/
|
|
7
7
|
/***/ (function(module) {
|
|
8
8
|
|
|
9
|
-
/*! @license DOMPurify 3.4.
|
|
9
|
+
/*! @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 */
|
|
10
10
|
(function(global, factory) {
|
|
11
11
|
true ? module.exports = factory() : 0;
|
|
12
12
|
})(this, function() {
|
|
@@ -134,9 +134,12 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
965
987
|
documentFragment: 11,
|
|
966
988
|
notation: 12
|
|
967
989
|
};
|
|
968
|
-
const
|
|
990
|
+
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
|
|
969
991
|
"style",
|
|
970
992
|
"script",
|
|
971
993
|
"xmp",
|
|
@@ -974,15 +996,7 @@
|
|
|
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 @@
|
|
|
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.
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1485
1504
|
* return. Return true otherwise.
|
|
1486
1505
|
*/
|
|
1487
1506
|
const _checkValidNamespace = function _checkValidNamespace(element) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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 @@
|
|
|
1540
1564
|
}
|
|
1541
1565
|
};
|
|
1542
1566
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
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
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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 @@
|
|
|
1577
1586
|
}
|
|
1578
1587
|
};
|
|
1579
1588
|
/**
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
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 @@
|
|
|
1622
1622
|
}
|
|
1623
1623
|
};
|
|
1624
1624
|
/**
|
|
1625
|
-
*
|
|
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 @@
|
|
|
1642
1638
|
}
|
|
1643
1639
|
};
|
|
1644
1640
|
/**
|
|
1645
|
-
*
|
|
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
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1679
|
-
|
|
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
|
-
*
|
|
1682
|
-
*
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
*
|
|
1699
|
-
*
|
|
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
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
|
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
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
|
|
1715
|
-
|
|
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 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
1822
|
+
return false;
|
|
1737
1823
|
}
|
|
1738
1824
|
if (nodeType === NODE_TYPE.element) {
|
|
1739
1825
|
const element = node;
|
|
@@ -1743,9 +1829,8 @@
|
|
|
1743
1829
|
if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
|
|
1744
1830
|
} catch (_) {}
|
|
1745
1831
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
}
|
|
1832
|
+
return true;
|
|
1833
|
+
});
|
|
1749
1834
|
};
|
|
1750
1835
|
/**
|
|
1751
1836
|
* _initDocument
|
|
@@ -1822,6 +1907,27 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
|
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 @@
|
|
|
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 @@
|
|
|
1986
2088
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
1987
2089
|
};
|
|
1988
2090
|
/**
|
|
1989
|
-
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
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
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
2217
2314
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
2218
2315
|
recreatedNamedProp = true;
|
|
2219
2316
|
}
|
|
2220
|
-
if (
|
|
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 @@
|
|
|
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
|
-
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
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 @@
|
|
|
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 @@
|
|
|
2365
2449
|
_neutralizeRoot(dirty);
|
|
2366
2450
|
throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
|
|
2367
2451
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
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 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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) {
|