Summary
braces 3.0.3 bounds only input character length (MAX_LENGTH = 10000), not brace nesting depth. Its recursive AST walkers use one stack frame per nesting level with no depth guard. A short, deeply-nested pattern under the character cap exhausts the call stack and throws RangeError: Maximum call stack size exceeded; uncaught, this terminates the Node process. Verified on Node v26.5.0 / braces@3.0.3: compile (the default braces() path) crashes at depth ≥ ~3500 and expand at ≥ ~4000, both within the 10,000-char cap.
- Component: braces
- Affected version: <= 3.0.3
- CWE: CWE-674 (Uncontrolled Recursion); CWE-400 (Uncontrolled Resource Consumption)
- Attack vector: Untrusted deeply-nested brace pattern from an unauthenticated caller
Description
parse() caps characters (lib/parse.js:37-39) but never nesting depth. Each nested { produces a brace node containing another brace node, and each output path walks the tree recursively:
// lib/compile.js:48-52 — default braces() path, no depth guard
if (node.nodes) {
for (const child of node.nodes) {
output += walk(child, node); // one frame per nesting level
}
}
The recursion is synchronous and driven purely by input depth. A few thousand nesting levels, far below the character cap, exceed the engine's stack limit. expand.js:102-104 shares the same recursion and crashes at depth ≥ ~4000. stringify.js:22-26 is structurally identical but does not overflow within the character cap (its lighter frames survive to the maximum depth of ~5000 the cap allows), so the exploitable sinks are compile and expand.
The README documents options.maxLength (default 10,000) only as a character guard, and its "Brace matching pitfalls" section covers only expansion output size. No depth bound is documented and there is no maxDepth option. These inputs stay under the character cap yet crash in default braces() compile mode, because the fault is in AST walk recursion. The library already validates malformed input via MAX_LENGTH; depth is the same class of guard, left incomplete.
Recommended Fix
- Enforce a maximum brace nesting depth in
parse() (e.g. throw above ~100), analogous to the existing character cap. This bounds every downstream walker at the source.
- Optionally convert the walk/stringify recursion to an explicit iterative stack.
- Until a fixed release: set
options.maxLength far below 10,000 and/or reject patterns whose { nesting exceeds a small bound before calling braces().
Reference
- CWE-674 (Uncontrolled Recursion); CWE-400 (Uncontrolled Resource Consumption)
- Related (distinct: token count vs. nesting depth): CVE-2024-4068 / GHSA-grv7-fg5c-xmjg
Summary
braces3.0.3 bounds only input character length (MAX_LENGTH = 10000), not brace nesting depth. Its recursive AST walkers use one stack frame per nesting level with no depth guard. A short, deeply-nested pattern under the character cap exhausts the call stack and throwsRangeError: Maximum call stack size exceeded; uncaught, this terminates the Node process. Verified on Node v26.5.0 /braces@3.0.3:compile(the defaultbraces()path) crashes at depth ≥ ~3500 andexpandat ≥ ~4000, both within the 10,000-char cap.Description
parse()caps characters (lib/parse.js:37-39) but never nesting depth. Each nested{produces abracenode containing anotherbracenode, and each output path walks the tree recursively:The recursion is synchronous and driven purely by input depth. A few thousand nesting levels, far below the character cap, exceed the engine's stack limit.
expand.js:102-104shares the same recursion and crashes at depth ≥ ~4000.stringify.js:22-26is structurally identical but does not overflow within the character cap (its lighter frames survive to the maximum depth of ~5000 the cap allows), so the exploitable sinks arecompileandexpand.The README documents
options.maxLength(default 10,000) only as a character guard, and its "Brace matching pitfalls" section covers only expansion output size. No depth bound is documented and there is nomaxDepthoption. These inputs stay under the character cap yet crash in defaultbraces()compile mode, because the fault is in AST walk recursion. The library already validates malformed input viaMAX_LENGTH; depth is the same class of guard, left incomplete.Recommended Fix
parse()(e.g. throw above ~100), analogous to the existing character cap. This bounds every downstream walker at the source.options.maxLengthfar below 10,000 and/or reject patterns whose{nesting exceeds a small bound before callingbraces().Reference