mirror of
https://github.com/hanzoai/DOMPurify.git
synced 2026-09-24 16:55:05 +00:00
fix: addressed a possible DoS with IN_PLACE and KEEP_CONTENT, thanks @n2duc (#1555)
fix: fixed an OSV scanner false alert by updating the toml
This commit is contained in:
Vendored
+18
-19
@@ -1582,7 +1582,7 @@ function createDOMPurify() {
|
||||
* @param tagName the node's transformCaseFunc'd tag name
|
||||
* @return true if the node was removed, false if kept
|
||||
*/
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
||||
/* Check if we have a custom element to handle */
|
||||
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
||||
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
||||
@@ -1605,27 +1605,26 @@ function createDOMPurify() {
|
||||
const childNodes = getChildNodes(currentNode);
|
||||
if (childNodes && parentNode) {
|
||||
const childCount = childNodes.length;
|
||||
/* In-place: hoist the *original* children so the iterator visits
|
||||
and sanitises them through the same allowlist pass as every other
|
||||
node. The caller built the tree in the live document, so the
|
||||
originals carry already-queued resource events (`<img onerror>`,
|
||||
`<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
|
||||
those originals detached but still armed, firing in page scope
|
||||
while the returned tree looked clean. Moving is safe in-place: the
|
||||
root is pre-validated as an allowed tag and so is never the node
|
||||
being removed, which keeps `parentNode` inside the iterator root
|
||||
and the relocated child inside the serialised tree.
|
||||
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
||||
at — and the result serialised from — `body`, so a restrictive
|
||||
ALLOWED_TAGS that removes `body` itself must leave its content in
|
||||
place, which only cloning does; and those paths parse into an
|
||||
inert document, so their discarded originals never had a queued
|
||||
event to neutralise.
|
||||
/* Hoist by moving each child up one level rather than deep-cloning
|
||||
it. Moving transfers every descendant exactly once, so a chain of
|
||||
nested disallowed elements costs O(n) instead of the O(n^2) that
|
||||
re-cloning the shrinking subtree at each level produced; it also
|
||||
empties the removed original, so `DOMPurify.removed` no longer
|
||||
pins whole subtrees. Moving preserves the in-place guarantee too:
|
||||
an original carrying already-queued resource events (`<img
|
||||
onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
|
||||
relocated and sanitised rather than left detached but still armed.
|
||||
The sole case that must clone is removing the walk root itself.
|
||||
The result is serialised from the root's subtree, so a restrictive
|
||||
ALLOWED_TAGS that strips the root (`body` on the string path) must
|
||||
leave the content inside it, which only cloning does. In IN_PLACE
|
||||
the root is pre-validated as an allowed tag and so is never removed
|
||||
here, so that path always takes the move branch.
|
||||
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
||||
valid whether we move (drops the trailing entry) or clone (leaves
|
||||
the list intact). */
|
||||
for (let i = childCount - 1; i >= 0; --i) {
|
||||
const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
|
||||
const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
|
||||
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
||||
}
|
||||
}
|
||||
@@ -1688,7 +1687,7 @@ function createDOMPurify() {
|
||||
}
|
||||
/* Remove element if anything forbids its presence */
|
||||
if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName);
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
|
||||
/* A false return means the node is a custom element kept via
|
||||
CUSTOM_ELEMENT_HANDLING - the only keep path through
|
||||
_sanitizeDisallowedNode. Run afterSanitizeElements on it so the
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+18
-19
@@ -1580,7 +1580,7 @@ function createDOMPurify() {
|
||||
* @param tagName the node's transformCaseFunc'd tag name
|
||||
* @return true if the node was removed, false if kept
|
||||
*/
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
||||
/* Check if we have a custom element to handle */
|
||||
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
||||
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
||||
@@ -1603,27 +1603,26 @@ function createDOMPurify() {
|
||||
const childNodes = getChildNodes(currentNode);
|
||||
if (childNodes && parentNode) {
|
||||
const childCount = childNodes.length;
|
||||
/* In-place: hoist the *original* children so the iterator visits
|
||||
and sanitises them through the same allowlist pass as every other
|
||||
node. The caller built the tree in the live document, so the
|
||||
originals carry already-queued resource events (`<img onerror>`,
|
||||
`<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
|
||||
those originals detached but still armed, firing in page scope
|
||||
while the returned tree looked clean. Moving is safe in-place: the
|
||||
root is pre-validated as an allowed tag and so is never the node
|
||||
being removed, which keeps `parentNode` inside the iterator root
|
||||
and the relocated child inside the serialised tree.
|
||||
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
||||
at — and the result serialised from — `body`, so a restrictive
|
||||
ALLOWED_TAGS that removes `body` itself must leave its content in
|
||||
place, which only cloning does; and those paths parse into an
|
||||
inert document, so their discarded originals never had a queued
|
||||
event to neutralise.
|
||||
/* Hoist by moving each child up one level rather than deep-cloning
|
||||
it. Moving transfers every descendant exactly once, so a chain of
|
||||
nested disallowed elements costs O(n) instead of the O(n^2) that
|
||||
re-cloning the shrinking subtree at each level produced; it also
|
||||
empties the removed original, so `DOMPurify.removed` no longer
|
||||
pins whole subtrees. Moving preserves the in-place guarantee too:
|
||||
an original carrying already-queued resource events (`<img
|
||||
onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
|
||||
relocated and sanitised rather than left detached but still armed.
|
||||
The sole case that must clone is removing the walk root itself.
|
||||
The result is serialised from the root's subtree, so a restrictive
|
||||
ALLOWED_TAGS that strips the root (`body` on the string path) must
|
||||
leave the content inside it, which only cloning does. In IN_PLACE
|
||||
the root is pre-validated as an allowed tag and so is never removed
|
||||
here, so that path always takes the move branch.
|
||||
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
||||
valid whether we move (drops the trailing entry) or clone (leaves
|
||||
the list intact). */
|
||||
for (let i = childCount - 1; i >= 0; --i) {
|
||||
const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
|
||||
const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
|
||||
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
||||
}
|
||||
}
|
||||
@@ -1686,7 +1685,7 @@ function createDOMPurify() {
|
||||
}
|
||||
/* Remove element if anything forbids its presence */
|
||||
if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName);
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
|
||||
/* A false return means the node is a custom element kept via
|
||||
CUSTOM_ELEMENT_HANDLING - the only keep path through
|
||||
_sanitizeDisallowedNode. Run afterSanitizeElements on it so the
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+18
-19
@@ -1586,7 +1586,7 @@
|
||||
* @param tagName the node's transformCaseFunc'd tag name
|
||||
* @return true if the node was removed, false if kept
|
||||
*/
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
|
||||
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
||||
/* Check if we have a custom element to handle */
|
||||
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
||||
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
||||
@@ -1609,27 +1609,26 @@
|
||||
const childNodes = getChildNodes(currentNode);
|
||||
if (childNodes && parentNode) {
|
||||
const childCount = childNodes.length;
|
||||
/* In-place: hoist the *original* children so the iterator visits
|
||||
and sanitises them through the same allowlist pass as every other
|
||||
node. The caller built the tree in the live document, so the
|
||||
originals carry already-queued resource events (`<img onerror>`,
|
||||
`<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
|
||||
those originals detached but still armed, firing in page scope
|
||||
while the returned tree looked clean. Moving is safe in-place: the
|
||||
root is pre-validated as an allowed tag and so is never the node
|
||||
being removed, which keeps `parentNode` inside the iterator root
|
||||
and the relocated child inside the serialised tree.
|
||||
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
||||
at — and the result serialised from — `body`, so a restrictive
|
||||
ALLOWED_TAGS that removes `body` itself must leave its content in
|
||||
place, which only cloning does; and those paths parse into an
|
||||
inert document, so their discarded originals never had a queued
|
||||
event to neutralise.
|
||||
/* Hoist by moving each child up one level rather than deep-cloning
|
||||
it. Moving transfers every descendant exactly once, so a chain of
|
||||
nested disallowed elements costs O(n) instead of the O(n^2) that
|
||||
re-cloning the shrinking subtree at each level produced; it also
|
||||
empties the removed original, so `DOMPurify.removed` no longer
|
||||
pins whole subtrees. Moving preserves the in-place guarantee too:
|
||||
an original carrying already-queued resource events (`<img
|
||||
onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
|
||||
relocated and sanitised rather than left detached but still armed.
|
||||
The sole case that must clone is removing the walk root itself.
|
||||
The result is serialised from the root's subtree, so a restrictive
|
||||
ALLOWED_TAGS that strips the root (`body` on the string path) must
|
||||
leave the content inside it, which only cloning does. In IN_PLACE
|
||||
the root is pre-validated as an allowed tag and so is never removed
|
||||
here, so that path always takes the move branch.
|
||||
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
||||
valid whether we move (drops the trailing entry) or clone (leaves
|
||||
the list intact). */
|
||||
for (let i = childCount - 1; i >= 0; --i) {
|
||||
const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
|
||||
const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
|
||||
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
||||
}
|
||||
}
|
||||
@@ -1692,7 +1691,7 @@
|
||||
}
|
||||
/* Remove element if anything forbids its presence */
|
||||
if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName);
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
|
||||
/* A false return means the node is a custom element kept via
|
||||
CUSTOM_ELEMENT_HANDLING - the only keep path through
|
||||
_sanitizeDisallowedNode. Run afterSanitizeElements on it so the
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+18
-6
@@ -19,12 +19,19 @@
|
||||
# The legacy-browser harness pins 1.30-1.50.1 on purpose and keeps its own
|
||||
# suppression at test/legacy-playwright/osv-scanner.toml.
|
||||
#
|
||||
# 2026-07: the two entries below are STOPGAPS, not verdicts. Both advisories
|
||||
# already have published fixes that the current dependency ranges accept,
|
||||
# and `npm update js-yaml brace-expansion` moves the tree to
|
||||
# 1.1.16 / 2.1.2 / 5.0.7 and 4.3.0 with the build and jsdom suite green.
|
||||
# Per the policy above, drop both entries once that lockfile refresh lands
|
||||
# rather than carrying them to the expiry date.
|
||||
# 2026-07: the first two entries below are STOPGAPS. Both advisories have
|
||||
# published fixes the current ranges accept, and `npm update js-yaml
|
||||
# brace-expansion` moves the tree to 1.1.16 / 2.1.2 / 5.0.8 and 4.3.0 with the
|
||||
# build and jsdom suite green. Per the policy above, drop those two once that
|
||||
# lockfile refresh lands rather than carrying them to the expiry date.
|
||||
#
|
||||
# The third entry (GHSA-mh99-v99m-4gvg) is different and is NOT cleared by a
|
||||
# refresh: its only fix is on the 5.x line (5.0.8). The top-level
|
||||
# brace-expansion updates to 5.0.8 and clears, but the transitive 1.x / 2.x
|
||||
# copies pulled in by eslint / typescript-estree / test-exclude top out at
|
||||
# 1.1.16 / 2.1.2 - both below 5.0.8, with no in-line backport - so they stay
|
||||
# flagged until those parents adopt brace-expansion 5.x (or the copies are
|
||||
# deduped out). Drop this entry then, not on a version bump of the dep alone.
|
||||
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-3jxr-9vmj-r5cp"
|
||||
@@ -35,3 +42,8 @@ reason = "brace-expansion (CVE-2026-13149): exponential-time expansion of consec
|
||||
id = "GHSA-52cp-r559-cp3m"
|
||||
ignoreUntil = 2027-07-22
|
||||
reason = "js-yaml (CVE-2026-59869): quadratic CPU time on a chain of mappings using merge keys, availability-only. Dev-only path - pulled in by eslint / cosmiconfig / nyc config loading at 4.2.0 (package.json already pins an override of ^4.2.0, which admits the fix). Only repo-owned config files are parsed, never untrusted YAML, and it is absent from the published artifact. Fixed in 4.3.0, and in 3.15.0 on the 3.x line."
|
||||
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-mh99-v99m-4gvg"
|
||||
ignoreUntil = 2027-07-22
|
||||
reason = "brace-expansion (CVE-2026-14257): unbounded expansion length causing an out-of-memory process crash, availability-only (CVSS 7.5). OSV range is [0, 5.0.8) with the fix on the 5.x line only. Dev-only path - the top-level copy updates to 5.0.8 and clears, but the transitive 1.1.16 / 2.1.2 copies (eslint / typescript-estree / test-exclude) have no in-line backport and stay below 5.0.8 until those parents adopt brace-expansion 5.x. DOMPurify ships no runtime dependencies, so this is absent from the published artifact, and CI only ever expands repo-owned glob patterns, never attacker-controlled input."
|
||||
|
||||
+22
-21
@@ -1779,7 +1779,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
||||
*/
|
||||
const _sanitizeDisallowedNode = function (
|
||||
currentNode: any,
|
||||
tagName: string
|
||||
tagName: string,
|
||||
root: Node
|
||||
): boolean {
|
||||
/* Check if we have a custom element to handle */
|
||||
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
||||
@@ -1813,31 +1814,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
||||
if (childNodes && parentNode) {
|
||||
const childCount = childNodes.length;
|
||||
|
||||
/* In-place: hoist the *original* children so the iterator visits
|
||||
and sanitises them through the same allowlist pass as every other
|
||||
node. The caller built the tree in the live document, so the
|
||||
originals carry already-queued resource events (`<img onerror>`,
|
||||
`<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
|
||||
those originals detached but still armed, firing in page scope
|
||||
while the returned tree looked clean. Moving is safe in-place: the
|
||||
root is pre-validated as an allowed tag and so is never the node
|
||||
being removed, which keeps `parentNode` inside the iterator root
|
||||
and the relocated child inside the serialised tree.
|
||||
/* Hoist by moving each child up one level rather than deep-cloning
|
||||
it. Moving transfers every descendant exactly once, so a chain of
|
||||
nested disallowed elements costs O(n) instead of the O(n^2) that
|
||||
re-cloning the shrinking subtree at each level produced; it also
|
||||
empties the removed original, so `DOMPurify.removed` no longer
|
||||
pins whole subtrees. Moving preserves the in-place guarantee too:
|
||||
an original carrying already-queued resource events (`<img
|
||||
onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
|
||||
relocated and sanitised rather than left detached but still armed.
|
||||
|
||||
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
||||
at — and the result serialised from — `body`, so a restrictive
|
||||
ALLOWED_TAGS that removes `body` itself must leave its content in
|
||||
place, which only cloning does; and those paths parse into an
|
||||
inert document, so their discarded originals never had a queued
|
||||
event to neutralise.
|
||||
The sole case that must clone is removing the walk root itself.
|
||||
The result is serialised from the root's subtree, so a restrictive
|
||||
ALLOWED_TAGS that strips the root (`body` on the string path) must
|
||||
leave the content inside it, which only cloning does. In IN_PLACE
|
||||
the root is pre-validated as an allowed tag and so is never removed
|
||||
here, so that path always takes the move branch.
|
||||
|
||||
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
||||
valid whether we move (drops the trailing entry) or clone (leaves
|
||||
the list intact). */
|
||||
for (let i = childCount - 1; i >= 0; --i) {
|
||||
const hoisted = IN_PLACE
|
||||
? childNodes[i]
|
||||
: cloneNode(childNodes[i], true);
|
||||
const hoisted =
|
||||
currentNode === root
|
||||
? cloneNode(childNodes[i], true)
|
||||
: childNodes[i];
|
||||
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
||||
}
|
||||
}
|
||||
@@ -1918,7 +1919,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
||||
) &&
|
||||
!ALLOWED_TAGS[tagName])
|
||||
) {
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName);
|
||||
const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
|
||||
|
||||
/* A false return means the node is a custom element kept via
|
||||
CUSTOM_ELEMENT_HANDLING - the only keep path through
|
||||
|
||||
Reference in New Issue
Block a user