dompurify 3.4.10 → 3.4.12
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 +27 -7
- package/dist/purify.cjs.d.ts +1 -1
- package/dist/purify.cjs.js +234 -28
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.cov.cjs.js +8041 -7944
- package/dist/purify.cov.cjs.js.map +1 -1
- package/dist/purify.es.d.mts +1 -1
- package/dist/purify.es.mjs +234 -28
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +234 -28
- 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 +16 -8
- package/src/attrs.ts +2 -0
- package/src/config.ts +0 -2
- package/src/purify.ts +275 -35
- package/src/types.ts +0 -2
package/src/purify.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/indent */
|
|
2
|
-
|
|
3
1
|
import type { Config, UseProfilesConfig } from './config';
|
|
4
2
|
import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
|
|
5
3
|
import * as TAGS from './tags.js';
|
|
@@ -435,6 +433,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
435
433
|
/* Track whether config is already set on this instance of DOMPurify. */
|
|
436
434
|
let SET_CONFIG = false;
|
|
437
435
|
|
|
436
|
+
/* Pristine allowlist bindings captured at setConfig() time. On the
|
|
437
|
+
* persistent-config path sanitize() restores the sets from these before
|
|
438
|
+
* the per-walk hook clone-guard, so a hook's in-call widening cannot
|
|
439
|
+
* carry across calls. Null until setConfig() is called; reset by
|
|
440
|
+
* clearConfig(). */
|
|
441
|
+
let SET_CONFIG_ALLOWED_TAGS = null;
|
|
442
|
+
let SET_CONFIG_ALLOWED_ATTR = null;
|
|
443
|
+
|
|
438
444
|
/* Decide if all elements (e.g. style, script) must be children of
|
|
439
445
|
* document.body. By default, browsers might move them to document.head */
|
|
440
446
|
let FORCE_BODY = false;
|
|
@@ -944,30 +950,6 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
944
950
|
}
|
|
945
951
|
}
|
|
946
952
|
|
|
947
|
-
/*
|
|
948
|
-
* Mirror the clone-before-mutate pattern already applied above for
|
|
949
|
-
* cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
|
|
950
|
-
* registered AND the set still points at the default constant,
|
|
951
|
-
* clone it. The hook then mutates the clone (in-call widening
|
|
952
|
-
* still works exactly as documented) and the next default-cfg
|
|
953
|
-
* call rebinds to the untouched original via the reassignment at
|
|
954
|
-
* the top of this function.
|
|
955
|
-
*/
|
|
956
|
-
if (
|
|
957
|
-
(hooks.uponSanitizeElement.length > 0 ||
|
|
958
|
-
hooks.uponSanitizeAttribute.length > 0) &&
|
|
959
|
-
ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS
|
|
960
|
-
) {
|
|
961
|
-
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (
|
|
965
|
-
hooks.uponSanitizeAttribute.length > 0 &&
|
|
966
|
-
ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR
|
|
967
|
-
) {
|
|
968
|
-
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
953
|
// Prevent further manipulation of configuration.
|
|
972
954
|
// Not available in IE8, Safari 5, etc.
|
|
973
955
|
if (freeze) {
|
|
@@ -1206,6 +1188,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1206
1188
|
* @param root the in-place root to empty
|
|
1207
1189
|
*/
|
|
1208
1190
|
const _neutralizeRoot = function (root: Node): void {
|
|
1191
|
+
/* Strip every disallowed attribute (on* handlers included) off the whole
|
|
1192
|
+
subtree BEFORE detaching anything. Detaching first would hand back
|
|
1193
|
+
handler-bearing originals (e.g. an already-loading `<img onerror>`)
|
|
1194
|
+
whose queued resource event still fires in page scope after we throw.
|
|
1195
|
+
Clobber-safe reads; a doomed clobbered node's own attributes are
|
|
1196
|
+
irrelevant while its non-clobbered descendants are reached and scrubbed. */
|
|
1197
|
+
_neutralizeSubtree(root);
|
|
1198
|
+
|
|
1209
1199
|
const childNodes = getChildNodes(root);
|
|
1210
1200
|
if (childNodes) {
|
|
1211
1201
|
const snapshot: Node[] = [];
|
|
@@ -1345,6 +1335,100 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1345
1335
|
}
|
|
1346
1336
|
};
|
|
1347
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* _neutralizePatchLinkage
|
|
1340
|
+
*
|
|
1341
|
+
* IN_PLACE entry pre-pass (declarative-partial-updates / streaming
|
|
1342
|
+
* hardening, https://github.com/WICG/declarative-partial-updates).
|
|
1343
|
+
*
|
|
1344
|
+
* The main walk strips patch linkage (`for`/`patchsrc`) and removes range
|
|
1345
|
+
* markers (PIs / markup comments) node-by-node, in document order, AS it
|
|
1346
|
+
* reaches each node. On a live in-place root that leaves a window: from the
|
|
1347
|
+
* moment the root is connected until the walk arrives at a given node, that
|
|
1348
|
+
* node's linkage is live. A patch applied on connection/stream can fire as
|
|
1349
|
+
* a microtask during the walk and inject or teleport an unsanitized DOM
|
|
1350
|
+
* range into a region the iterator has already passed and will not revisit,
|
|
1351
|
+
* so the post-return "tree is sanitized" contract is violated. Sweep the
|
|
1352
|
+
* whole tree once up front and sever every linkage before the walk begins,
|
|
1353
|
+
* closing that window.
|
|
1354
|
+
*
|
|
1355
|
+
* This CANNOT undo a patch that already fired before sanitize ran — that is
|
|
1356
|
+
* the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
|
|
1357
|
+
* but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
|
|
1358
|
+
* to group with the rest of the declarative-partial-updates handling and
|
|
1359
|
+
* stay overridable, consistent with the codebase.
|
|
1360
|
+
*
|
|
1361
|
+
* Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
|
|
1362
|
+
* clobbered root cannot defeat the sweep of its non-clobbered descendants.
|
|
1363
|
+
*
|
|
1364
|
+
* NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
|
|
1365
|
+
* .html Q1): this mirrors the existing policy of keeping `for` on
|
|
1366
|
+
* <label>/<output>. If the shipping feature can drive a patch through a
|
|
1367
|
+
* surviving `for`-on-label/output + `id` pair, this pre-pass and the
|
|
1368
|
+
* attribute check at _isBasicCustomElement's caller must additionally drop
|
|
1369
|
+
* that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
|
|
1370
|
+
*
|
|
1371
|
+
* @param root the in-place root to sweep
|
|
1372
|
+
*/
|
|
1373
|
+
const _neutralizePatchLinkage = function (root: Node): void {
|
|
1374
|
+
if (!SAFE_FOR_XML) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const stack: Node[] = [root];
|
|
1379
|
+
while (stack.length > 0) {
|
|
1380
|
+
const node = stack.pop();
|
|
1381
|
+
const nodeType = getNodeType ? getNodeType(node) : (node as any).nodeType;
|
|
1382
|
+
|
|
1383
|
+
/* Remove range markers (the target side of a patch linkage): every
|
|
1384
|
+
processing instruction, and any markup-bearing comment. */
|
|
1385
|
+
if (
|
|
1386
|
+
nodeType === NODE_TYPE.processingInstruction ||
|
|
1387
|
+
(nodeType === NODE_TYPE.comment &&
|
|
1388
|
+
regExpTest(EXPRESSIONS.COMMENT_MARKUP_PROBE, (node as any).data))
|
|
1389
|
+
) {
|
|
1390
|
+
try {
|
|
1391
|
+
remove(node);
|
|
1392
|
+
} catch (_) {
|
|
1393
|
+
/* Best-effort */
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/* Strip patch-source attributes (the source side) off elements. */
|
|
1400
|
+
if (nodeType === NODE_TYPE.element) {
|
|
1401
|
+
const element = node as Element;
|
|
1402
|
+
const lcTag = transformCaseFunc(
|
|
1403
|
+
getNodeName ? getNodeName(node) : (node as any).nodeName
|
|
1404
|
+
);
|
|
1405
|
+
try {
|
|
1406
|
+
if (element.hasAttribute && element.hasAttribute('patchsrc')) {
|
|
1407
|
+
element.removeAttribute('patchsrc');
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (
|
|
1411
|
+
element.hasAttribute &&
|
|
1412
|
+
element.hasAttribute('for') &&
|
|
1413
|
+
lcTag !== 'label' &&
|
|
1414
|
+
lcTag !== 'output'
|
|
1415
|
+
) {
|
|
1416
|
+
element.removeAttribute('for');
|
|
1417
|
+
}
|
|
1418
|
+
} catch (_) {
|
|
1419
|
+
/* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const childNodes = getChildNodes(node);
|
|
1424
|
+
if (childNodes) {
|
|
1425
|
+
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
1426
|
+
stack.push(childNodes[i]);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1348
1432
|
/**
|
|
1349
1433
|
* _initDocument
|
|
1350
1434
|
*
|
|
@@ -1679,9 +1763,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1679
1763
|
/**
|
|
1680
1764
|
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1681
1765
|
* allowed custom elements (false return exits _sanitizeElements
|
|
1682
|
-
* early - namespace
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1766
|
+
* early - the namespace and fallback-tag removal checks are
|
|
1767
|
+
* intentionally skipped for kept custom elements), else hoist
|
|
1768
|
+
* content per KEEP_CONTENT and remove.
|
|
1769
|
+
*
|
|
1770
|
+
* A kept custom element is the ONLY case in which this function
|
|
1771
|
+
* returns false, so the caller uses that return value to run the
|
|
1772
|
+
* afterSanitizeElements hook on the kept element and keep the
|
|
1773
|
+
* element-hook lifecycle consistent with normal allowlisted
|
|
1774
|
+
* elements (GHSA-c2j3-45gr-mqc4).
|
|
1685
1775
|
*
|
|
1686
1776
|
* @param currentNode the disallowed node
|
|
1687
1777
|
* @param tagName the node's transformCaseFunc'd tag name
|
|
@@ -1766,10 +1856,17 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1766
1856
|
* @param currentNode to check for permission to exist
|
|
1767
1857
|
* @return true if node was killed, false if left alive
|
|
1768
1858
|
*/
|
|
1769
|
-
|
|
1859
|
+
// eslint-disable-next-line complexity
|
|
1860
|
+
const _sanitizeElements = function (currentNode: any, root: Node): boolean {
|
|
1770
1861
|
/* Execute a hook if present */
|
|
1771
1862
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1772
1863
|
|
|
1864
|
+
/* A hook may have detached the node — treat it as removed (see the
|
|
1865
|
+
detached-node comment after the uponSanitizeElement hook below). */
|
|
1866
|
+
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1773
1870
|
/* Check if element is clobbered or can clobber */
|
|
1774
1871
|
if (_isClobbered(currentNode)) {
|
|
1775
1872
|
_forceRemove(currentNode);
|
|
@@ -1787,6 +1884,25 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1787
1884
|
allowedTags: ALLOWED_TAGS,
|
|
1788
1885
|
});
|
|
1789
1886
|
|
|
1887
|
+
/* A hook may have detached the node from the tree — a long-standing
|
|
1888
|
+
user pattern (issue #469; draw.io-style foreignObject filtering).
|
|
1889
|
+
Per the cached, unclobberable parentNode getter the node is
|
|
1890
|
+
genuinely out of the tree, so it can reach neither the serialized
|
|
1891
|
+
output nor an IN_PLACE live tree; treat it as removed and stop
|
|
1892
|
+
processing it. Without this guard, the unsafe-node / namespace
|
|
1893
|
+
checks below would call _forceRemove on a parentless node and hit
|
|
1894
|
+
the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
|
|
1895
|
+
wants gone but *cannot* detach (clobbered / parentless roots), the
|
|
1896
|
+
opposite of a node that is already safely gone. The walk root is
|
|
1897
|
+
exempt: a detached IN_PLACE root is legitimate input and must still
|
|
1898
|
+
be fully sanitized, and a kill-decision on it must keep hitting the
|
|
1899
|
+
REPORT-3 throw. Nodes detached by hooks are the hook's
|
|
1900
|
+
responsibility: they are not recorded in DOMPurify.removed and are
|
|
1901
|
+
not neutralized by the post-walk IN_PLACE pass. */
|
|
1902
|
+
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1790
1906
|
/* Remove mXSS vectors, processing instructions and risky comments */
|
|
1791
1907
|
if (_isUnsafeNode(currentNode, tagName)) {
|
|
1792
1908
|
_forceRemove(currentNode);
|
|
@@ -1802,7 +1918,24 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1802
1918
|
) &&
|
|
1803
1919
|
!ALLOWED_TAGS[tagName])
|
|
1804
1920
|
) {
|
|
1805
|
-
|
|
1921
|
+
const removed = _sanitizeDisallowedNode(currentNode, tagName);
|
|
1922
|
+
|
|
1923
|
+
/* A false return means the node is a custom element kept via
|
|
1924
|
+
CUSTOM_ELEMENT_HANDLING - the only keep path through
|
|
1925
|
+
_sanitizeDisallowedNode. Run afterSanitizeElements on it so the
|
|
1926
|
+
element-hook lifecycle matches normal allowlisted elements: a
|
|
1927
|
+
security policy applied in this hook (e.g. stripping an attribute
|
|
1928
|
+
from every surviving element) must not silently skip kept custom
|
|
1929
|
+
elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
|
|
1930
|
+
tail below - the hook runs, then the walker's subsequent
|
|
1931
|
+
_sanitizeAttributes pass sanitizes the element's attributes. The
|
|
1932
|
+
deliberately skipped namespace and fallback-tag removal checks stay
|
|
1933
|
+
skipped; they are removal decisions, not the hook contract. */
|
|
1934
|
+
if (removed === false) {
|
|
1935
|
+
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
return removed;
|
|
1806
1939
|
}
|
|
1807
1940
|
|
|
1808
1941
|
/* Check whether element has a valid namespace.
|
|
@@ -1864,6 +1997,42 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1864
1997
|
return false;
|
|
1865
1998
|
}
|
|
1866
1999
|
|
|
2000
|
+
/* Reject declarative-partial-updates patch-linkage attributes
|
|
2001
|
+
(https://github.com/WICG/declarative-partial-updates).
|
|
2002
|
+
|
|
2003
|
+
Empirical note (Chrome 150, verified — see
|
|
2004
|
+
test/declarative-patch-probe-v3.html): expansion is NOT applied after
|
|
2005
|
+
sanitization. For the string path it fires during sanitize()'s own
|
|
2006
|
+
parse, so the walk sees and sanitizes the fully materialized expanded
|
|
2007
|
+
tree — teleports into MathML/SVG integration points included; a
|
|
2008
|
+
weaponized `<template for>`->`<img onerror>` comes back with the handler
|
|
2009
|
+
stripped. For the IN_PLACE path it fires on connection, before the walk.
|
|
2010
|
+
Either way DOMPurify is NOT blind to the patch.
|
|
2011
|
+
|
|
2012
|
+
This removal is therefore defense-in-depth rather than the sole barrier:
|
|
2013
|
+
it prevents live linkage from surviving into the OUTPUT and re-expanding
|
|
2014
|
+
in the caller's context, and keeps behaviour deterministic if a future
|
|
2015
|
+
engine defers expansion. `for` is legitimate only on <label>/<output>;
|
|
2016
|
+
anywhere else (notably <template for>) it links the element to a patch
|
|
2017
|
+
target and teleports or removes an arbitrary DOM range by id/marker name.
|
|
2018
|
+
`patchsrc` fetches remote markup and is treated as a script-loading
|
|
2019
|
+
mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
|
|
2020
|
+
other structural-threat checks and stays overridable, consistent with
|
|
2021
|
+
the rest of the codebase. PI range markers are already removed by
|
|
2022
|
+
_isUnsafeNode. */
|
|
2023
|
+
if (SAFE_FOR_XML && lcName === 'patchsrc') {
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (
|
|
2028
|
+
SAFE_FOR_XML &&
|
|
2029
|
+
lcName === 'for' &&
|
|
2030
|
+
lcTag !== 'label' &&
|
|
2031
|
+
lcTag !== 'output'
|
|
2032
|
+
) {
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1867
2036
|
/* Make sure attribute cannot clobber */
|
|
1868
2037
|
if (
|
|
1869
2038
|
SANITIZE_DOM &&
|
|
@@ -2203,7 +2372,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2203
2372
|
_executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
|
|
2204
2373
|
|
|
2205
2374
|
/* Sanitize tags and elements */
|
|
2206
|
-
_sanitizeElements(shadowNode);
|
|
2375
|
+
_sanitizeElements(shadowNode, fragment);
|
|
2207
2376
|
|
|
2208
2377
|
/* Check attributes next */
|
|
2209
2378
|
_sanitizeAttributes(shadowNode);
|
|
@@ -2366,10 +2535,37 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2366
2535
|
}
|
|
2367
2536
|
|
|
2368
2537
|
/* Assign config vars */
|
|
2369
|
-
if (
|
|
2538
|
+
if (SET_CONFIG) {
|
|
2539
|
+
/* Persistent setConfig() path: _parseConfig is skipped, so the sets are
|
|
2540
|
+
* not re-derived per call. Restore them from the pristine bindings
|
|
2541
|
+
* captured at setConfig() time so a previous call's hook clone (mutated
|
|
2542
|
+
* below) does not carry over. */
|
|
2543
|
+
ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
|
|
2544
|
+
ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
|
|
2545
|
+
} else {
|
|
2370
2546
|
_parseConfig(cfg);
|
|
2371
2547
|
}
|
|
2372
2548
|
|
|
2549
|
+
/* Clone the hook-mutable allowlists before the walk whenever an
|
|
2550
|
+
* uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
|
|
2551
|
+
* and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
|
|
2552
|
+
* a hook that widens them would otherwise mutate the shared set
|
|
2553
|
+
* permanently: across later calls and across every element. Cloning per
|
|
2554
|
+
* walk keeps documented in-call widening working while scoping it to the
|
|
2555
|
+
* call. A single guard for both config paths - the per-call path rebinds
|
|
2556
|
+
* the sets in _parseConfig each call, the persistent path restores them
|
|
2557
|
+
* from the captured bindings just above - so the two cannot diverge. */
|
|
2558
|
+
if (
|
|
2559
|
+
hooks.uponSanitizeElement.length > 0 ||
|
|
2560
|
+
hooks.uponSanitizeAttribute.length > 0
|
|
2561
|
+
) {
|
|
2562
|
+
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
if (hooks.uponSanitizeAttribute.length > 0) {
|
|
2566
|
+
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2373
2569
|
/* Clean up removed elements */
|
|
2374
2570
|
DOMPurify.removed = [];
|
|
2375
2571
|
|
|
@@ -2382,6 +2578,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2382
2578
|
const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
|
|
2383
2579
|
|
|
2384
2580
|
if (inPlace) {
|
|
2581
|
+
/* Declarative-partial-updates / streaming pre-pass: sever every patch
|
|
2582
|
+
linkage across the live tree BEFORE the walk, so no patch can fire
|
|
2583
|
+
mid-walk and inject into an already-processed region. Runs first, so
|
|
2584
|
+
it also covers the forbidden/clobbered roots that throw below. */
|
|
2585
|
+
_neutralizePatchLinkage(dirty as Node);
|
|
2586
|
+
|
|
2385
2587
|
/* Do some early pre-sanitization to avoid unsafe root nodes.
|
|
2386
2588
|
Read nodeName through the cached prototype getter — a clobbering
|
|
2387
2589
|
child named "nodeName" on the form root would otherwise shadow
|
|
@@ -2393,6 +2595,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2393
2595
|
if (typeof nn === 'string') {
|
|
2394
2596
|
const tagName = transformCaseFunc(nn);
|
|
2395
2597
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
2598
|
+
/* Fail closed on a live root: neutralize handlers/children before
|
|
2599
|
+
throwing, exactly as the mid-walk abort path does. */
|
|
2600
|
+
_neutralizeRoot(dirty as Node);
|
|
2601
|
+
|
|
2396
2602
|
throw typeErrorCreate(
|
|
2397
2603
|
'root node is forbidden and cannot be sanitized in-place'
|
|
2398
2604
|
);
|
|
@@ -2410,6 +2616,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2410
2616
|
the application unsanitized. Refuse to sanitize such a root
|
|
2411
2617
|
the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
|
|
2412
2618
|
if (_isClobbered(dirty as Element)) {
|
|
2619
|
+
/* Fail closed on a live clobbered root before throwing.
|
|
2620
|
+
_neutralizeRoot's reads are clobber-safe (cached getters); the
|
|
2621
|
+
form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
|
|
2622
|
+
_neutralizeRoot(dirty as Node);
|
|
2623
|
+
|
|
2413
2624
|
throw typeErrorCreate(
|
|
2414
2625
|
'root node is clobbered and cannot be sanitized in-place'
|
|
2415
2626
|
);
|
|
@@ -2480,7 +2691,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2480
2691
|
}
|
|
2481
2692
|
|
|
2482
2693
|
/* Get node iterator */
|
|
2483
|
-
const
|
|
2694
|
+
const walkRoot: Node = inPlace ? (dirty as Node) : body;
|
|
2695
|
+
const nodeIterator = _createNodeIterator(walkRoot);
|
|
2484
2696
|
|
|
2485
2697
|
/* Now start iterating over the created document.
|
|
2486
2698
|
The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
|
|
@@ -2494,7 +2706,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2494
2706
|
try {
|
|
2495
2707
|
while ((currentNode = nodeIterator.nextNode())) {
|
|
2496
2708
|
/* Sanitize tags and elements */
|
|
2497
|
-
_sanitizeElements(currentNode);
|
|
2709
|
+
_sanitizeElements(currentNode, walkRoot);
|
|
2498
2710
|
|
|
2499
2711
|
/* Check attributes next */
|
|
2500
2712
|
_sanitizeAttributes(currentNode);
|
|
@@ -2510,6 +2722,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2510
2722
|
} catch (error) {
|
|
2511
2723
|
if (inPlace) {
|
|
2512
2724
|
_neutralizeRoot(dirty as Node);
|
|
2725
|
+
/* Nodes _forceRemove'd earlier in the aborted walk are already
|
|
2726
|
+
detached from the root, so _neutralizeRoot's subtree pass does not
|
|
2727
|
+
reach them. Defuse them too, mirroring the success-path loop below. */
|
|
2728
|
+
arrayForEach(DOMPurify.removed, (entry) => {
|
|
2729
|
+
if (entry.element) {
|
|
2730
|
+
_neutralizeSubtree(entry.element as Node);
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2513
2733
|
}
|
|
2514
2734
|
|
|
2515
2735
|
throw error;
|
|
@@ -2596,11 +2816,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2596
2816
|
DOMPurify.setConfig = function (cfg = {}) {
|
|
2597
2817
|
_parseConfig(cfg);
|
|
2598
2818
|
SET_CONFIG = true;
|
|
2819
|
+
SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
|
|
2820
|
+
SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
|
|
2599
2821
|
};
|
|
2600
2822
|
|
|
2601
2823
|
DOMPurify.clearConfig = function () {
|
|
2602
2824
|
CONFIG = null;
|
|
2603
2825
|
SET_CONFIG = false;
|
|
2826
|
+
SET_CONFIG_ALLOWED_TAGS = null;
|
|
2827
|
+
SET_CONFIG_ALLOWED_ATTR = null;
|
|
2604
2828
|
|
|
2605
2829
|
// Drop any caller-supplied Trusted Types policy so it cannot poison later
|
|
2606
2830
|
// `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
|
|
@@ -2629,6 +2853,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2629
2853
|
return;
|
|
2630
2854
|
}
|
|
2631
2855
|
|
|
2856
|
+
/* Reject unknown entry points. Without this, a non-hook key (e.g.
|
|
2857
|
+
* '__proto__') indexes off the prototype chain rather than a real
|
|
2858
|
+
* hook array, and arrayPush then writes to Object.prototype. Guard
|
|
2859
|
+
* with an own-property check against the known hook names. */
|
|
2860
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2632
2864
|
arrayPush(hooks[entryPoint], hookFunction);
|
|
2633
2865
|
};
|
|
2634
2866
|
|
|
@@ -2636,6 +2868,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2636
2868
|
entryPoint: keyof HooksMap,
|
|
2637
2869
|
hookFunction: HookFunction
|
|
2638
2870
|
) {
|
|
2871
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2872
|
+
return undefined;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2639
2875
|
if (hookFunction !== undefined) {
|
|
2640
2876
|
const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
|
|
2641
2877
|
|
|
@@ -2648,6 +2884,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2648
2884
|
};
|
|
2649
2885
|
|
|
2650
2886
|
DOMPurify.removeHooks = function (entryPoint: keyof HooksMap) {
|
|
2887
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2651
2891
|
hooks[entryPoint] = [];
|
|
2652
2892
|
};
|
|
2653
2893
|
|