dompurify 3.4.16 → 3.4.17
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 +9 -2
- package/dist/purify.cjs.d.ts +1 -1
- package/dist/purify.cjs.js +272 -196
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.es.d.mts +1 -1
- package/dist/purify.es.mjs +272 -196
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +272 -196
- package/dist/purify.js.map +1 -1
- package/dist/purify.min.js +3 -3
- package/dist/purify.min.js.map +1 -1
- package/package.json +5 -3
- package/src/purify.ts +465 -363
- package/src/utils.ts +36 -0
package/dist/purify.es.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @license DOMPurify 3.4.
|
|
1
|
+
/*! @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 */
|
|
2
2
|
function _OverloadYield(e, d) {
|
|
3
3
|
this.v = e, this.k = d;
|
|
4
4
|
}
|
|
@@ -122,9 +122,12 @@ Array.prototype.indexOf;
|
|
|
122
122
|
const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
|
|
123
123
|
const arrayPop = unapply(Array.prototype.pop);
|
|
124
124
|
const arrayPush = unapply(Array.prototype.push);
|
|
125
|
-
Array.prototype.slice;
|
|
125
|
+
const arraySlice = unapply(Array.prototype.slice);
|
|
126
126
|
const arraySplice = unapply(Array.prototype.splice);
|
|
127
127
|
const arrayIsArray = Array.isArray;
|
|
128
|
+
const setCreate = unconstruct(Set);
|
|
129
|
+
const setAdd = unapply(Set.prototype.add);
|
|
130
|
+
const setHas = unapply(Set.prototype.has);
|
|
128
131
|
const stringToLowerCase = unapply(String.prototype.toLowerCase);
|
|
129
132
|
const stringToString = unapply(String.prototype.toString);
|
|
130
133
|
const stringMatch = unapply(String.prototype.match);
|
|
@@ -269,6 +272,25 @@ function lookupGetter(object, prop) {
|
|
|
269
272
|
}
|
|
270
273
|
return fallbackValue;
|
|
271
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Like lookupGetter, but for accessor properties only, and returns null
|
|
277
|
+
* instead of a null-returning fallback when no getter exists on the chain.
|
|
278
|
+
* Lets a caller fall back to a direct property read in environments that
|
|
279
|
+
* lack the getter, where a fallback returning null would be indistinguishable
|
|
280
|
+
* from a legitimate null value (e.g. namespaceURI of a no-namespace element).
|
|
281
|
+
*
|
|
282
|
+
* @param object - The object (usually a prototype) to start the lookup at.
|
|
283
|
+
* @param prop - The accessor property name.
|
|
284
|
+
* @returns The unapplied getter, or null if none was found.
|
|
285
|
+
*/
|
|
286
|
+
function lookupAccessor(object, prop) {
|
|
287
|
+
while (object !== null) {
|
|
288
|
+
const desc = getOwnPropertyDescriptor(object, prop);
|
|
289
|
+
if (desc) return desc.get ? unapply(desc.get) : null;
|
|
290
|
+
object = getPrototypeOf(object);
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
272
294
|
function isRegex(value) {
|
|
273
295
|
try {
|
|
274
296
|
regExpTest(value, "");
|
|
@@ -953,7 +975,7 @@ const NODE_TYPE = {
|
|
|
953
975
|
documentFragment: 11,
|
|
954
976
|
notation: 12
|
|
955
977
|
};
|
|
956
|
-
const
|
|
978
|
+
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, [
|
|
957
979
|
"style",
|
|
958
980
|
"script",
|
|
959
981
|
"xmp",
|
|
@@ -962,15 +984,7 @@ const LITERAL_TEXT_ELEMENT_NAMES = [
|
|
|
962
984
|
"noframes",
|
|
963
985
|
"plaintext",
|
|
964
986
|
"noscript"
|
|
965
|
-
];
|
|
966
|
-
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
|
|
967
|
-
const LITERAL_TEXT_CLOSE = function() {
|
|
968
|
-
const map = {};
|
|
969
|
-
arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
|
|
970
|
-
map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
|
|
971
|
-
});
|
|
972
|
-
return freeze(map);
|
|
973
|
-
}();
|
|
987
|
+
]));
|
|
974
988
|
const getGlobal = function getGlobal() {
|
|
975
989
|
return typeof window === "undefined" ? null : window;
|
|
976
990
|
};
|
|
@@ -1048,7 +1062,7 @@ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallbac
|
|
|
1048
1062
|
function createDOMPurify() {
|
|
1049
1063
|
let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
1050
1064
|
const DOMPurify = (root) => createDOMPurify(root);
|
|
1051
|
-
DOMPurify.version = "3.4.
|
|
1065
|
+
DOMPurify.version = "3.4.17";
|
|
1052
1066
|
DOMPurify.removed = [];
|
|
1053
1067
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
1054
1068
|
DOMPurify.isSupported = false;
|
|
@@ -1080,6 +1094,10 @@ function createDOMPurify() {
|
|
|
1080
1094
|
const _readNodeName = function _readNodeName(node) {
|
|
1081
1095
|
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
1082
1096
|
};
|
|
1097
|
+
const getNamespaceURI = lookupAccessor(ElementPrototype, "namespaceURI");
|
|
1098
|
+
const _readNamespaceURI = function _readNamespaceURI(element) {
|
|
1099
|
+
return getNamespaceURI ? getNamespaceURI(element) : element.namespaceURI;
|
|
1100
|
+
};
|
|
1083
1101
|
if (typeof HTMLTemplateElement === "function") {
|
|
1084
1102
|
const template = document.createElement("template");
|
|
1085
1103
|
if (template.content && template.content.ownerDocument) document = template.content.ownerDocument;
|
|
@@ -1200,6 +1218,7 @@ function createDOMPurify() {
|
|
|
1200
1218
|
const SANITIZE_NAMED_PROPS_PREFIX = "user-content-";
|
|
1201
1219
|
let KEEP_CONTENT = true;
|
|
1202
1220
|
let IN_PLACE = false;
|
|
1221
|
+
let IN_PLACE_VISITED = null;
|
|
1203
1222
|
let USE_PROFILES = {};
|
|
1204
1223
|
let FORBID_CONTENTS = null;
|
|
1205
1224
|
const DEFAULT_FORBID_CONTENTS = addToSet({}, [
|
|
@@ -1473,18 +1492,23 @@ function createDOMPurify() {
|
|
|
1473
1492
|
* return. Return true otherwise.
|
|
1474
1493
|
*/
|
|
1475
1494
|
const _checkValidNamespace = function _checkValidNamespace(element) {
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1495
|
+
const elementNamespace = _readNamespaceURI(element);
|
|
1496
|
+
const tagName = stringToLowerCase(_readNodeName(element));
|
|
1497
|
+
const parentNode = getParentNode(element);
|
|
1498
|
+
let parent;
|
|
1499
|
+
let parentTagName;
|
|
1500
|
+
if (parentNode && _readNodeType(parentNode) === NODE_TYPE.element) {
|
|
1501
|
+
parent = { namespaceURI: _readNamespaceURI(parentNode) };
|
|
1502
|
+
parentTagName = stringToLowerCase(_readNodeName(parentNode));
|
|
1503
|
+
} else {
|
|
1504
|
+
parent = { namespaceURI: NAMESPACE };
|
|
1505
|
+
parentTagName = "template";
|
|
1506
|
+
}
|
|
1507
|
+
if (!ALLOWED_NAMESPACES[elementNamespace]) return false;
|
|
1508
|
+
if (elementNamespace === SVG_NAMESPACE) return _checkSvgNamespace(tagName, parent, parentTagName);
|
|
1509
|
+
if (elementNamespace === MATHML_NAMESPACE) return _checkMathMlNamespace(tagName, parent, parentTagName);
|
|
1510
|
+
if (elementNamespace === HTML_NAMESPACE) return _checkHtmlNamespace(tagName, parent, parentTagName);
|
|
1511
|
+
if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[elementNamespace]) return true;
|
|
1488
1512
|
return false;
|
|
1489
1513
|
};
|
|
1490
1514
|
/**
|
|
@@ -1528,36 +1552,21 @@ function createDOMPurify() {
|
|
|
1528
1552
|
}
|
|
1529
1553
|
};
|
|
1530
1554
|
/**
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
|
|
1535
|
-
* custom element's reaction detaches a node so `_forceRemove`'s deliberate
|
|
1536
|
-
* parentless guard throws, or any other re-entrant engine mutation — would
|
|
1537
|
-
* otherwise leave the caller's *live* tree half-sanitized, with everything
|
|
1538
|
-
* after the abort point still carrying its handlers. There is no safe way
|
|
1539
|
-
* to resume the walk (the tree mutated under us), so we strip the root bare:
|
|
1540
|
-
* remove every child and every attribute, then let the caller's catch see
|
|
1541
|
-
* the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
|
|
1542
|
-
* getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
|
|
1555
|
+
* Fail-closed teardown of an in-place root: strip every disallowed attribute
|
|
1556
|
+
* in the subtree first, so nothing armed is left behind, then remove all
|
|
1557
|
+
* children and the root's own attributes.
|
|
1543
1558
|
*
|
|
1544
1559
|
* @param root the in-place root to empty
|
|
1545
1560
|
*/
|
|
1546
1561
|
const _neutralizeRoot = function _neutralizeRoot(root) {
|
|
1547
1562
|
_neutralizeSubtree(root);
|
|
1548
1563
|
const childNodes = getChildNodes(root);
|
|
1549
|
-
if (childNodes) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
try {
|
|
1556
|
-
remove(child);
|
|
1557
|
-
} catch (_) {}
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
const attributes = getAttributes(root);
|
|
1564
|
+
if (childNodes) arrayForEach(arraySlice(childNodes), (child) => {
|
|
1565
|
+
try {
|
|
1566
|
+
remove(child);
|
|
1567
|
+
} catch (_) {}
|
|
1568
|
+
});
|
|
1569
|
+
const attributes = _readNodeType(root) === NODE_TYPE.element ? getAttributes(root) : null;
|
|
1561
1570
|
if (attributes) for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1562
1571
|
const attribute = attributes[i];
|
|
1563
1572
|
const name = attribute && attribute.name;
|
|
@@ -1565,18 +1574,9 @@ function createDOMPurify() {
|
|
|
1565
1574
|
}
|
|
1566
1575
|
};
|
|
1567
1576
|
/**
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
* lookup key for HTML elements in an HTML document, so they silently miss an
|
|
1572
|
-
* attribute whose stored qualified name still contains uppercase ASCII
|
|
1573
|
-
* letters. That happens when the node came from a case-preserving source
|
|
1574
|
-
* (an XML/XHTML document imported via importNode(), or createAttributeNS()),
|
|
1575
|
-
* where e.g. `ONERROR` survives the walk: the policy check lowercases to
|
|
1576
|
-
* `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
|
|
1577
|
-
* `onerror` and finds nothing. Remove the exact Attr node instead, which is
|
|
1578
|
-
* case- and namespace-exact, and fall back to name-based removal only when
|
|
1579
|
-
* the caller could not supply the node.
|
|
1577
|
+
* Removes an attribute, preferably as its exact Attr node: name-based
|
|
1578
|
+
* removal lowercases the name in HTML documents and misses case-preserved
|
|
1579
|
+
* names, e.g. from imported XHTML.
|
|
1580
1580
|
*
|
|
1581
1581
|
* @param name an Attribute name
|
|
1582
1582
|
* @param element a DOM node
|
|
@@ -1610,12 +1610,8 @@ function createDOMPurify() {
|
|
|
1610
1610
|
}
|
|
1611
1611
|
};
|
|
1612
1612
|
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
* Removes every attribute the active configuration does not allow from a
|
|
1616
|
-
* single element, using the same allowlist as the main attribute pass (so
|
|
1617
|
-
* `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
|
|
1618
|
-
* neutralise nodes that are being discarded from an in-place tree.
|
|
1613
|
+
* Removes every attribute the configuration does not allow. Used to
|
|
1614
|
+
* neutralize nodes discarded from an in-place tree.
|
|
1619
1615
|
*
|
|
1620
1616
|
* @param element the element to strip
|
|
1621
1617
|
*/
|
|
@@ -1630,77 +1626,158 @@ function createDOMPurify() {
|
|
|
1630
1626
|
}
|
|
1631
1627
|
};
|
|
1632
1628
|
/**
|
|
1633
|
-
*
|
|
1629
|
+
* Iterative depth-first walk, so deep trees cannot overflow the call stack,
|
|
1630
|
+
* reading through the cached getters, so clobbering cannot hide children.
|
|
1631
|
+
* When `visit` returns false, the node's children are skipped. With
|
|
1632
|
+
* `intoShadow`, open shadow roots are walked after their host's children.
|
|
1634
1633
|
*
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
1638
|
-
* all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
|
|
1639
|
-
* those dropped nodes are detached from the caller's LIVE tree but a
|
|
1640
|
-
* handler-bearing original among them (an `<img onerror>`/`<video>` that was
|
|
1641
|
-
* loading) keeps its queued resource event, which fires in page scope after
|
|
1642
|
-
* sanitize returns. This walks a removed subtree and strips every attribute
|
|
1643
|
-
* the active configuration does not allow — so `on*` handlers are cancelled
|
|
1644
|
-
* through the SAME allowlist that governs kept nodes, not a separate `/^on/`
|
|
1645
|
-
* blocklist. Run synchronously before sanitize returns, i.e. before any
|
|
1646
|
-
* queued event can fire. Hook-free by design: these nodes leave the output,
|
|
1647
|
-
* so firing attribute hooks for them would be surprising. Clobber-safe reads;
|
|
1648
|
-
* a doomed clobbered node may shadow `removeAttribute` (its own attributes are
|
|
1649
|
-
* irrelevant — it is discarded — while its non-clobbered descendants, e.g.
|
|
1650
|
-
* the `<img>`, are reached and scrubbed).
|
|
1651
|
-
*
|
|
1652
|
-
* @param root the root of a removed subtree to neutralise
|
|
1634
|
+
* @param root where to start
|
|
1635
|
+
* @param visit called for every node
|
|
1636
|
+
* @param intoShadow whether to enter open shadow roots
|
|
1653
1637
|
*/
|
|
1654
|
-
const
|
|
1638
|
+
const _walkTree = function _walkTree(root, visit) {
|
|
1639
|
+
let intoShadow = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
|
|
1655
1640
|
const stack = [root];
|
|
1656
1641
|
while (stack.length > 0) {
|
|
1657
1642
|
const node = stack.pop();
|
|
1658
|
-
if (
|
|
1643
|
+
if (visit(node) === false) continue;
|
|
1644
|
+
if (intoShadow && _readNodeType(node) === NODE_TYPE.element) {
|
|
1645
|
+
const shadowRoot = getShadowRoot(node);
|
|
1646
|
+
if (_isDocumentFragment(shadowRoot)) stack.push(shadowRoot);
|
|
1647
|
+
}
|
|
1659
1648
|
const childNodes = getChildNodes(node);
|
|
1660
1649
|
if (childNodes) for (let i = childNodes.length - 1; i >= 0; --i) stack.push(childNodes[i]);
|
|
1661
1650
|
}
|
|
1662
1651
|
};
|
|
1663
1652
|
/**
|
|
1664
|
-
*
|
|
1653
|
+
* Strips disallowed attributes from every element of a discarded subtree,
|
|
1654
|
+
* so a handler that is already queued (a loading image) cannot fire.
|
|
1665
1655
|
*
|
|
1666
|
-
*
|
|
1667
|
-
|
|
1656
|
+
* @param root the root of a removed subtree
|
|
1657
|
+
*/
|
|
1658
|
+
const _neutralizeSubtree = function _neutralizeSubtree(root) {
|
|
1659
|
+
_walkTree(root, (node) => {
|
|
1660
|
+
if (_readNodeType(node) === NODE_TYPE.element) _stripDisallowedAttributes(node);
|
|
1661
|
+
});
|
|
1662
|
+
};
|
|
1663
|
+
/**
|
|
1664
|
+
* All nodes under `root`, root first, including open shadow roots. Template
|
|
1665
|
+
* content is not entered, it is inert.
|
|
1668
1666
|
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1667
|
+
* @param root the in-place root
|
|
1668
|
+
* @return the nodes
|
|
1669
|
+
*/
|
|
1670
|
+
const _collectLiveNodes = function _collectLiveNodes(root) {
|
|
1671
|
+
const nodes = [];
|
|
1672
|
+
_walkTree(root, (node) => {
|
|
1673
|
+
nodes.push(node);
|
|
1674
|
+
}, true);
|
|
1675
|
+
return nodes;
|
|
1676
|
+
};
|
|
1677
|
+
/**
|
|
1678
|
+
* Strips disallowed attributes from every element that was under the
|
|
1679
|
+
* in-place root when the call started and no longer is, whatever removed
|
|
1680
|
+
* or moved it.
|
|
1679
1681
|
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1682
|
+
* @param root the in-place root
|
|
1683
|
+
* @param before the root's nodes, collected before the walk
|
|
1684
|
+
*/
|
|
1685
|
+
const _neutralizeEscapedElements = function _neutralizeEscapedElements(root, before) {
|
|
1686
|
+
const reachable = setCreate();
|
|
1687
|
+
arrayForEach(_collectLiveNodes(root), (node) => {
|
|
1688
|
+
if (_readNodeType(node) === NODE_TYPE.element) setAdd(reachable, node);
|
|
1689
|
+
});
|
|
1690
|
+
arrayForEach(before, (node) => {
|
|
1691
|
+
if (_readNodeType(node) === NODE_TYPE.element && !setHas(reachable, node)) _stripDisallowedAttributes(node);
|
|
1692
|
+
});
|
|
1693
|
+
};
|
|
1694
|
+
/**
|
|
1695
|
+
* Neutralizes everything that left the in-place tree during the call: what
|
|
1696
|
+
* DOMPurify removed, and what hooks or custom elements removed or moved out.
|
|
1685
1697
|
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1698
|
+
* @param root the in-place root
|
|
1699
|
+
* @param before the root's nodes, collected before the walk
|
|
1700
|
+
* @return true if the root itself was removed
|
|
1701
|
+
*/
|
|
1702
|
+
const _neutralizeDiscarded = function _neutralizeDiscarded(root, before) {
|
|
1703
|
+
let rootRemoved = false;
|
|
1704
|
+
arrayForEach(DOMPurify.removed, (entry) => {
|
|
1705
|
+
if (entry.element) {
|
|
1706
|
+
rootRemoved = rootRemoved || entry.element === root;
|
|
1707
|
+
_neutralizeSubtree(entry.element);
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
_neutralizeEscapedElements(root, before);
|
|
1711
|
+
return rootRemoved;
|
|
1712
|
+
};
|
|
1713
|
+
const MAX_UNVISITED_ROUNDS = 8;
|
|
1714
|
+
/**
|
|
1715
|
+
* The walk only moves forward, so a node moved to a position it already
|
|
1716
|
+
* passed is never visited. Give every original node that is still in the
|
|
1717
|
+
* tree but unvisited the normal treatment, hooks included. Nodes created
|
|
1718
|
+
* during the call are left alone. Fails closed if the tree keeps changing.
|
|
1688
1719
|
*
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1720
|
+
* @param root the in-place root
|
|
1721
|
+
* @param before the root's nodes, collected before the walk
|
|
1722
|
+
*/
|
|
1723
|
+
const _sanitizeUnvisitedNodes = function _sanitizeUnvisitedNodes(root, before) {
|
|
1724
|
+
const visited = IN_PLACE_VISITED;
|
|
1725
|
+
let original = null;
|
|
1726
|
+
const getOriginal = () => {
|
|
1727
|
+
if (!original) {
|
|
1728
|
+
original = setCreate();
|
|
1729
|
+
arrayForEach(before, (beforeNode) => {
|
|
1730
|
+
setAdd(original, beforeNode);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
return original;
|
|
1734
|
+
};
|
|
1735
|
+
for (let round = 0;; round++) {
|
|
1736
|
+
const live = _collectLiveNodes(root);
|
|
1737
|
+
const containers = [root];
|
|
1738
|
+
let pending = false;
|
|
1739
|
+
arrayForEach(live, (node) => {
|
|
1740
|
+
if (_readNodeType(node) === NODE_TYPE.documentFragment) arrayPush(containers, node);
|
|
1741
|
+
else if (!pending && !setHas(visited, node)) pending = setHas(getOriginal(), node);
|
|
1742
|
+
});
|
|
1743
|
+
if (!pending) return;
|
|
1744
|
+
if (round >= MAX_UNVISITED_ROUNDS) throw typeErrorCreate("the tree kept changing during in-place sanitization; refusing to return it");
|
|
1745
|
+
arrayForEach(containers, (container, index) => {
|
|
1746
|
+
_sanitizeUnvisitedIn(container, index > 0, root, getOriginal());
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
/**
|
|
1751
|
+
* One _sanitizeUnvisitedNodes pass over the root or an open shadow root.
|
|
1695
1752
|
*
|
|
1696
|
-
* @param
|
|
1753
|
+
* @param container the root or an open shadow root
|
|
1754
|
+
* @param inShadow whether container is a shadow root
|
|
1755
|
+
* @param root the in-place root
|
|
1756
|
+
* @param original the root's nodes collected before the walk, as a set
|
|
1697
1757
|
*/
|
|
1758
|
+
const _sanitizeUnvisitedIn = function _sanitizeUnvisitedIn(container, inShadow, root, original) {
|
|
1759
|
+
const iterator = _createNodeIterator(container);
|
|
1760
|
+
let node;
|
|
1761
|
+
while (node = iterator.nextNode()) {
|
|
1762
|
+
if (!setHas(original, node) || setHas(IN_PLACE_VISITED, node)) continue;
|
|
1763
|
+
if (inShadow) _executeHooks(hooks.uponSanitizeShadowNode, node, null);
|
|
1764
|
+
_sanitizeElements(node, root);
|
|
1765
|
+
_sanitizeAttributes(node, root);
|
|
1766
|
+
if (_isDocumentFragment(node.content)) _sanitizeShadowDOM2(node.content);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1698
1769
|
/**
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
|
|
1703
|
-
|
|
1770
|
+
* Records that _sanitizeElements processed `node`. No-op outside IN_PLACE.
|
|
1771
|
+
*
|
|
1772
|
+
* @param node the node being processed
|
|
1773
|
+
*/
|
|
1774
|
+
const _markVisited = function _markVisited(node) {
|
|
1775
|
+
if (IN_PLACE_VISITED) setAdd(IN_PLACE_VISITED, node);
|
|
1776
|
+
};
|
|
1777
|
+
/**
|
|
1778
|
+
* Patch-linkage attributes (declarative partial updates): `patchsrc`
|
|
1779
|
+
* always, `for` everywhere except on <label> and <output>. Gated on
|
|
1780
|
+
* SAFE_FOR_XML. Shared by the IN_PLACE pre-pass and _isValidAttribute.
|
|
1704
1781
|
*
|
|
1705
1782
|
* @param lcName the transformCaseFunc'd attribute name
|
|
1706
1783
|
* @param lcTag the transformCaseFunc'd tag name of the carrying element
|
|
@@ -1711,17 +1788,26 @@ function createDOMPurify() {
|
|
|
1711
1788
|
if (lcName === "patchsrc") return true;
|
|
1712
1789
|
return lcName === "for" && lcTag !== "label" && lcTag !== "output";
|
|
1713
1790
|
};
|
|
1791
|
+
/**
|
|
1792
|
+
* IN_PLACE pre-pass: sever all patch linkage before the walk, so no patch
|
|
1793
|
+
* can fire mid-walk into a part of the tree the walker already passed.
|
|
1794
|
+
* Removes range markers (processing instructions, comments with markup)
|
|
1795
|
+
* and strips linkage attributes. It cannot undo a patch that already fired.
|
|
1796
|
+
*
|
|
1797
|
+
* TODO: if the shipped feature can patch through `for` on <label>/<output>,
|
|
1798
|
+
* drop that pair here too (see test/declarative-patch-probe.html).
|
|
1799
|
+
*
|
|
1800
|
+
* @param root the in-place root to sweep
|
|
1801
|
+
*/
|
|
1714
1802
|
const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
|
|
1715
1803
|
if (!SAFE_FOR_XML) return;
|
|
1716
|
-
|
|
1717
|
-
while (stack.length > 0) {
|
|
1718
|
-
const node = stack.pop();
|
|
1804
|
+
_walkTree(root, (node) => {
|
|
1719
1805
|
const nodeType = _readNodeType(node);
|
|
1720
1806
|
if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
|
|
1721
1807
|
try {
|
|
1722
1808
|
remove(node);
|
|
1723
1809
|
} catch (_) {}
|
|
1724
|
-
|
|
1810
|
+
return false;
|
|
1725
1811
|
}
|
|
1726
1812
|
if (nodeType === NODE_TYPE.element) {
|
|
1727
1813
|
const element = node;
|
|
@@ -1731,9 +1817,8 @@ function createDOMPurify() {
|
|
|
1731
1817
|
if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) element.removeAttribute("for");
|
|
1732
1818
|
} catch (_) {}
|
|
1733
1819
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1820
|
+
return true;
|
|
1821
|
+
});
|
|
1737
1822
|
};
|
|
1738
1823
|
/**
|
|
1739
1824
|
* _initDocument
|
|
@@ -1810,6 +1895,27 @@ function createDOMPurify() {
|
|
|
1810
1895
|
*/
|
|
1811
1896
|
const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
|
|
1812
1897
|
var _node$querySelectorAl;
|
|
1898
|
+
_scrubOwnCharacterData(node);
|
|
1899
|
+
const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
|
|
1900
|
+
if (templates) arrayForEach(templates, (tmpl) => {
|
|
1901
|
+
if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
|
|
1902
|
+
});
|
|
1903
|
+
};
|
|
1904
|
+
/**
|
|
1905
|
+
* _scrubOwnCharacterData
|
|
1906
|
+
*
|
|
1907
|
+
* Non-recursive core of _scrubTemplateExpressions: normalize `node` and
|
|
1908
|
+
* strip template expressions from its own character data only. Does not
|
|
1909
|
+
* enter <template>.content or attached shadow roots (NodeIterator never
|
|
1910
|
+
* does). Used by _sanitizeShadowDOM, where every nested template content
|
|
1911
|
+
* and shadow root is already walked - and therefore scrubbed - by its own
|
|
1912
|
+
* recursive _sanitizeShadowDOM call; recursing here as well would rescrub
|
|
1913
|
+
* each nested fragment once per ancestor, which is quadratic in nesting
|
|
1914
|
+
* depth.
|
|
1915
|
+
*
|
|
1916
|
+
* @param node The root whose own character data should be scrubbed.
|
|
1917
|
+
*/
|
|
1918
|
+
const _scrubOwnCharacterData = function _scrubOwnCharacterData(node) {
|
|
1813
1919
|
node.normalize();
|
|
1814
1920
|
const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
|
|
1815
1921
|
const walker = createNodeIterator.call(doc || node, node, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
|
|
@@ -1818,10 +1924,6 @@ function createDOMPurify() {
|
|
|
1818
1924
|
currentNode.data = _stripTemplateExpressions(currentNode.data);
|
|
1819
1925
|
currentNode = walker.nextNode();
|
|
1820
1926
|
}
|
|
1821
|
-
const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
|
|
1822
|
-
if (templates) arrayForEach(templates, (tmpl) => {
|
|
1823
|
-
if (_isDocumentFragment(tmpl.content)) _scrubTemplateExpressions2(tmpl.content);
|
|
1824
|
-
});
|
|
1825
1927
|
};
|
|
1826
1928
|
/**
|
|
1827
1929
|
* _isClobbered
|
|
@@ -1897,7 +1999,7 @@ function createDOMPurify() {
|
|
|
1897
1999
|
*/
|
|
1898
2000
|
const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
|
|
1899
2001
|
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) return true;
|
|
1900
|
-
if (SAFE_FOR_XML && currentNode
|
|
2002
|
+
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;
|
|
1901
2003
|
if (currentNode.nodeType === NODE_TYPE.processingInstruction) return true;
|
|
1902
2004
|
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) return true;
|
|
1903
2005
|
return false;
|
|
@@ -1941,7 +2043,7 @@ function createDOMPurify() {
|
|
|
1941
2043
|
*/
|
|
1942
2044
|
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
|
1943
2045
|
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) return false;
|
|
1944
|
-
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
2046
|
+
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName] && !FORBID_CONTENTS[stringToLowerCase(tagName)]) {
|
|
1945
2047
|
const parentNode = getParentNode(currentNode);
|
|
1946
2048
|
const childNodes = getChildNodes(currentNode);
|
|
1947
2049
|
if (childNodes && parentNode) {
|
|
@@ -1974,35 +2076,13 @@ function createDOMPurify() {
|
|
|
1974
2076
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
1975
2077
|
};
|
|
1976
2078
|
/**
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
*
|
|
1980
|
-
* filtering). Per the cached, unclobberable parentNode getter the node is
|
|
1981
|
-
* genuinely out of the tree, so it can reach neither the serialized
|
|
1982
|
-
* output nor an IN_PLACE live tree; treat it as removed and stop
|
|
1983
|
-
* processing it. Without this guard, the unsafe-node / namespace checks
|
|
1984
|
-
* would call _forceRemove on a parentless node and hit the REPORT-3
|
|
1985
|
-
* fail-closed throw — which exists for nodes DOMPurify wants gone but
|
|
1986
|
-
* *cannot* detach (clobbered / parentless roots), the opposite of a node
|
|
1987
|
-
* that is already safely gone. The walk root is exempt: a detached
|
|
1988
|
-
* IN_PLACE root is legitimate input and must still be fully sanitized,
|
|
1989
|
-
* and a kill-decision on it must keep hitting the REPORT-3 throw.
|
|
1990
|
-
*
|
|
1991
|
-
* Nodes detached by hooks stay the hook's responsibility for placement:
|
|
1992
|
-
* they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
|
|
1993
|
-
* pass (which iterates DOMPurify.removed) does not reach them. But a
|
|
1994
|
-
* hook-detached subtree can still hold a queued resource-event handler -
|
|
1995
|
-
* e.g. an <img onload> that began loading when the caller built the live
|
|
1996
|
-
* tree - which fires in page scope after sanitize returns even though the
|
|
1997
|
-
* handler never reached the returned tree. That is the audit-5 F1 hazard,
|
|
1998
|
-
* and the documented node.remove() hook pattern walks straight into it.
|
|
1999
|
-
* So on the IN_PLACE path we neutralize the detached subtree inline,
|
|
2000
|
-
* stripping its non-allow-listed attributes before returning, exactly as
|
|
2001
|
-
* the post-walk pass does for _forceRemove'd subtrees.
|
|
2079
|
+
* A hook may have detached the node it was called for. If so, treat it as
|
|
2080
|
+
* removed and stop processing it; the walk root is exempt. On the IN_PLACE
|
|
2081
|
+
* path the detached subtree is neutralized right away.
|
|
2002
2082
|
*
|
|
2003
2083
|
* @param currentNode the node a hook may have detached
|
|
2004
2084
|
* @param root the current walk root
|
|
2005
|
-
* @return true if the node
|
|
2085
|
+
* @return true if the node was detached
|
|
2006
2086
|
*/
|
|
2007
2087
|
const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
|
|
2008
2088
|
if (currentNode === root || getParentNode(currentNode) !== null) return false;
|
|
@@ -2019,6 +2099,7 @@ function createDOMPurify() {
|
|
|
2019
2099
|
* @return true if node was killed, false if left alive
|
|
2020
2100
|
*/
|
|
2021
2101
|
const _sanitizeElements = function _sanitizeElements(currentNode, root) {
|
|
2102
|
+
_markVisited(currentNode);
|
|
2022
2103
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
2023
2104
|
if (_handleHookDetachedNode(currentNode, root)) return true;
|
|
2024
2105
|
if (_isClobbered(currentNode)) {
|
|
@@ -2162,6 +2243,22 @@ function createDOMPurify() {
|
|
|
2162
2243
|
}
|
|
2163
2244
|
};
|
|
2164
2245
|
/**
|
|
2246
|
+
* _isUnsafeAttributeValue
|
|
2247
|
+
*
|
|
2248
|
+
* Value-based attribute checks that the attribute walk applies before any
|
|
2249
|
+
* hook can force-keep an attribute, shared with the public
|
|
2250
|
+
* isValidAttribute() so that it never approves a value sanitize() would
|
|
2251
|
+
* strip.
|
|
2252
|
+
*
|
|
2253
|
+
* @param name the attribute name as found on the node (case preserved)
|
|
2254
|
+
* @param value the attribute value
|
|
2255
|
+
* @returns true if the attribute must be removed because of its value
|
|
2256
|
+
*/
|
|
2257
|
+
const _isUnsafeAttributeValue = function _isUnsafeAttributeValue(name, value) {
|
|
2258
|
+
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) return true;
|
|
2259
|
+
return stringToLowerCase(name) === "attributename" && stringMatch(value, "href") !== null;
|
|
2260
|
+
};
|
|
2261
|
+
/**
|
|
2165
2262
|
* _sanitizeAttributes
|
|
2166
2263
|
*
|
|
2167
2264
|
* @protect attributes
|
|
@@ -2205,11 +2302,7 @@ function createDOMPurify() {
|
|
|
2205
2302
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
2206
2303
|
recreatedNamedProp = true;
|
|
2207
2304
|
}
|
|
2208
|
-
if (
|
|
2209
|
-
_removeAttribute(name, currentNode, attr);
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
if (lcName === "attributename" && stringMatch(value, "href")) {
|
|
2305
|
+
if (_isUnsafeAttributeValue(name, value)) {
|
|
2213
2306
|
_removeAttribute(name, currentNode, attr);
|
|
2214
2307
|
continue;
|
|
2215
2308
|
}
|
|
@@ -2257,24 +2350,13 @@ function createDOMPurify() {
|
|
|
2257
2350
|
}
|
|
2258
2351
|
}
|
|
2259
2352
|
}
|
|
2353
|
+
if (SAFE_FOR_TEMPLATES) _scrubOwnCharacterData(fragment);
|
|
2260
2354
|
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
|
|
2261
2355
|
};
|
|
2262
2356
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2266
|
-
* the existing _sanitizeShadowDOM pipeline. The default node iterator
|
|
2267
|
-
* does not descend into shadow trees, so nodes inside an attached
|
|
2268
|
-
* shadow root would otherwise be skipped entirely.
|
|
2269
|
-
*
|
|
2270
|
-
* Two real input paths put attached shadow roots in front of us:
|
|
2271
|
-
* 1. IN_PLACE on a DOM node that already has shadow roots attached.
|
|
2272
|
-
* 2. DOM-node input where importNode(dirty, true) deep-clones the
|
|
2273
|
-
* shadow root because it was created with `clonable: true`.
|
|
2274
|
-
*
|
|
2275
|
-
* This pass runs once, up front, so the main iteration loop (and the
|
|
2276
|
-
* existing _sanitizeShadowDOM template-content recursion) stay
|
|
2277
|
-
* untouched — string-input paths are not affected.
|
|
2357
|
+
* Sanitizes every attached shadow root under `root`, since the node
|
|
2358
|
+
* iterator does not enter shadow trees. Needed for IN_PLACE input and for
|
|
2359
|
+
* DOM input whose clonable shadow roots were copied by importNode().
|
|
2278
2360
|
*
|
|
2279
2361
|
* @param root the subtree root to walk for attached shadow roots
|
|
2280
2362
|
*/
|
|
@@ -2339,6 +2421,8 @@ function createDOMPurify() {
|
|
|
2339
2421
|
if (hooks.uponSanitizeAttribute.length > 0) ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
2340
2422
|
DOMPurify.removed = [];
|
|
2341
2423
|
const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
|
|
2424
|
+
let inPlaceNodes = [];
|
|
2425
|
+
const outerVisited = IN_PLACE_VISITED;
|
|
2342
2426
|
if (inPlace) {
|
|
2343
2427
|
_neutralizePatchLinkage(dirty);
|
|
2344
2428
|
const nn = _readNodeName(dirty);
|
|
@@ -2353,12 +2437,8 @@ function createDOMPurify() {
|
|
|
2353
2437
|
_neutralizeRoot(dirty);
|
|
2354
2438
|
throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
|
|
2355
2439
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
} catch (error) {
|
|
2359
|
-
_neutralizeRoot(dirty);
|
|
2360
|
-
throw error;
|
|
2361
|
-
}
|
|
2440
|
+
inPlaceNodes = _collectLiveNodes(dirty);
|
|
2441
|
+
IN_PLACE_VISITED = setCreate();
|
|
2362
2442
|
} else if (_isNode(dirty)) {
|
|
2363
2443
|
body = _initDocument("<!---->");
|
|
2364
2444
|
importedNode = body.ownerDocument.importNode(dirty, true);
|
|
@@ -2374,30 +2454,25 @@ function createDOMPurify() {
|
|
|
2374
2454
|
if (body && FORCE_BODY) _forceRemove(body.firstChild);
|
|
2375
2455
|
const walkRoot = inPlace ? dirty : body;
|
|
2376
2456
|
try {
|
|
2457
|
+
if (inPlace) _sanitizeAttachedShadowRoots(dirty);
|
|
2377
2458
|
const nodeIterator = _createNodeIterator(walkRoot);
|
|
2378
2459
|
while (currentNode = nodeIterator.nextNode()) {
|
|
2379
2460
|
_sanitizeElements(currentNode, walkRoot);
|
|
2380
2461
|
_sanitizeAttributes(currentNode, walkRoot);
|
|
2381
2462
|
if (_isDocumentFragment(currentNode.content)) _sanitizeShadowDOM2(currentNode.content);
|
|
2382
2463
|
}
|
|
2464
|
+
if (inPlace) _sanitizeUnvisitedNodes(dirty, inPlaceNodes);
|
|
2383
2465
|
} catch (error) {
|
|
2384
2466
|
if (inPlace) {
|
|
2385
2467
|
_neutralizeRoot(dirty);
|
|
2386
|
-
|
|
2387
|
-
if (entry.element) _neutralizeSubtree(entry.element);
|
|
2388
|
-
});
|
|
2468
|
+
_neutralizeDiscarded(dirty, inPlaceNodes);
|
|
2389
2469
|
}
|
|
2390
2470
|
throw error;
|
|
2471
|
+
} finally {
|
|
2472
|
+
IN_PLACE_VISITED = outerVisited;
|
|
2391
2473
|
}
|
|
2392
2474
|
if (inPlace) {
|
|
2393
|
-
|
|
2394
|
-
arrayForEach(DOMPurify.removed, (entry) => {
|
|
2395
|
-
if (entry.element) {
|
|
2396
|
-
if (entry.element === dirty) rootWasRemoved = true;
|
|
2397
|
-
_neutralizeSubtree(entry.element);
|
|
2398
|
-
}
|
|
2399
|
-
});
|
|
2400
|
-
if (rootWasRemoved) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
|
|
2475
|
+
if (_neutralizeDiscarded(dirty, inPlaceNodes)) throw typeErrorCreate("a node selected for removal could not be safely returned; refusing to sanitize in place");
|
|
2401
2476
|
if (SAFE_FOR_TEMPLATES) _scrubTemplateExpressions2(dirty);
|
|
2402
2477
|
return dirty;
|
|
2403
2478
|
}
|
|
@@ -2434,6 +2509,7 @@ function createDOMPurify() {
|
|
|
2434
2509
|
if (!CONFIG) _parseConfig({});
|
|
2435
2510
|
const lcTag = transformCaseFunc(tag);
|
|
2436
2511
|
const lcName = transformCaseFunc(attr);
|
|
2512
|
+
if (typeof value === "string" && (_isUnsafeAttributeValue(attr, value) || !ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value))) return false;
|
|
2437
2513
|
return _isValidAttribute(lcTag, lcName, value);
|
|
2438
2514
|
};
|
|
2439
2515
|
DOMPurify.addHook = function(entryPoint, hookFunction) {
|