Skip to content

Commit 5bc698c

Browse files
committed
Improve fix CVE-2026-9358 (NVD) / SNYK-JS-POSTCSSSELECTORPARSER-16873882 (clone/walk)
1 parent db32327 commit 5bc698c

3 files changed

Lines changed: 33 additions & 7 deletions

File tree

src/__tests__/recursion.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ ava("serializing a deeply nested AST throws instead of overflowing the stack", (
4747
t.false(error instanceof RangeError, "should be a controlled error, not a stack overflow");
4848
});
4949

50+
ava("cloning a deeply nested AST throws instead of overflowing the stack", (t) => {
51+
const deep = buildDeepAst(1000);
52+
const error = t.throws(() => deep.clone(), { instanceOf: Error });
53+
t.false(error instanceof RangeError, "should be a controlled error, not a stack overflow");
54+
});
55+
56+
ava("walking a deeply nested AST throws instead of overflowing the stack", (t) => {
57+
const deep = buildDeepAst(1000);
58+
const error = t.throws(() => deep.walk(() => {}), { instanceOf: Error });
59+
t.false(error instanceof RangeError, "should be a controlled error, not a stack overflow");
60+
});
61+
5062
ava("maxNestingDepth option controls the limit in both directions", (t) => {
5163
const input = nest(40);
5264
// A low limit rejects it and reports the configured value...

src/selectors/container.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolveMaxNestingDepth } from "../util";
1+
import { resolveMaxNestingDepth, MAX_NESTING_DEPTH } from "../util";
22
import Node from "./node";
33
import * as types from "./types";
44

@@ -200,12 +200,19 @@ export default class Container extends Node {
200200
}
201201
}
202202

203-
walk(callback) {
203+
walk(callback, depth = 0) {
204+
// Bound recursion so a pathologically deep node tree raises a catchable
205+
// error instead of overflowing the call stack (CVE-2026-9358 / CWE-674).
206+
if (depth > MAX_NESTING_DEPTH) {
207+
throw new Error(
208+
`Cannot walk selector: nesting depth exceeds the maximum of ${MAX_NESTING_DEPTH}.`,
209+
);
210+
}
204211
return this.each((node, i) => {
205212
let result = callback(node, i);
206213

207214
if (result !== false && node.length) {
208-
result = node.walk(callback);
215+
result = node.walk(callback, depth + 1);
209216
}
210217

211218
if (result === false) {

src/selectors/node.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { ensureObject } from "../util";
1+
import { ensureObject, MAX_NESTING_DEPTH } from "../util";
22

3-
let cloneNode = function (obj, parent) {
3+
let cloneNode = function (obj, parent, depth = 0) {
4+
// Bound recursion so a pathologically deep node tree raises a catchable
5+
// error instead of overflowing the call stack (CVE-2026-9358 / CWE-674).
6+
if (depth > MAX_NESTING_DEPTH) {
7+
throw new Error(
8+
`Cannot clone selector: nesting depth exceeds the maximum of ${MAX_NESTING_DEPTH}.`,
9+
);
10+
}
411
if (typeof obj !== "object" || obj === null) {
512
return obj;
613
}
@@ -19,9 +26,9 @@ let cloneNode = function (obj, parent) {
1926
cloned[i] = parent;
2027
}
2128
} else if (value instanceof Array) {
22-
cloned[i] = value.map((j) => cloneNode(j, cloned));
29+
cloned[i] = value.map((j) => cloneNode(j, cloned, depth + 1));
2330
} else {
24-
cloned[i] = cloneNode(value, cloned);
31+
cloned[i] = cloneNode(value, cloned, depth + 1);
2532
}
2633
}
2734

0 commit comments

Comments
 (0)