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