GitHub Advisoryadvisory · npm
GHSA-8pfc-jjgw-6g26 SandboxJS: Stack overflow DoS via deeply nested expressions in recursive descent parser
MEDIUM
Official page ↗
- Aliases
- CVE-2026-34211
- Ecosystem
- npm
- Packages
- @nyariv/sandboxjs
- Severity
- Moderate
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
- Weaknesses
- CWE-674
- Reviewed
- 2026-04-03
Affected packages
• npm @nyariv/sandboxjs: < 0.8.36 → fixed in 0.8.36
Description
Summary
The `@nyariv/sandboxjs` parser contains unbounded recursion in the `restOfExp` function and the `lispify`/`lispifyExpr` call chain. An attacker can crash any Node.js process that parses untrusted input by supplying deeply nested expressions (e.g., ~2000 nested parentheses), causing a `RangeError: Maximum call stack size exceeded` that terminates the process.
Details
The root cause is in `src/parser.ts`. The `restOfExp` function (line 443) iterates through expression characters, and when it encounters a closing bracket that doesn't match the expected `firstOpening`, it recursively calls itself at line 503:
```typescript
// src/parser.ts:486-505
} else if (closings[char]) {
// ...
if (char === firstOpening) {
done = true;
break;
} else {
const skip = restOfExp(constants, part.substring(i + 1), [], char); // line 503
cache.set(skip.start - 1, skip.end);
i += skip.length + 1;
}
}
```
Each nested bracket (`(`, `[`, `{`) adds a stack frame. There is no depth counter or limit check. The function signature has no depth parameter:
```typescript
export function restOfExp(
constants: IConstants,
part: CodeString,
tests?: RegExp[],
quote?: string,
firstOpening?: string,
closingsTests?: RegExp[],
details: restDetails = {},
): CodeString {
```
A second unbounded recursive path exists through `lispify` → `lispTypes.get(type)` → `group` handler → `lispifyExpr` (line 672) → `lispify`, which processes parenthesized groups recursively with no depth limit.
All public API methods (`Sandbox.parse()`, `Sandbox.compile()`, `Sandbox.compileAsync()`, `Sandbox.compileExpression()`, `Sandbox.compileExpressionAsync()`) pass user input directly to `parse()` with no input validation or depth limiting.
A `RangeError: Maximum call stack size exceeded` in Node.js is not a catchable exception in the normal sense — it crashes the current execution context and, in a server handling requests synchronously, can crash the entire process.
PoC
```bash
Install the package
npm install @nyariv/sandboxjs
Create test file
cat > poc.js MAX_PARSE_DEPTH) {
throw new ParseError('Expression nesting depth exceeded', part.toString());
}
// ... existing code ...
// At line 503, pass depth + 1:
const skip = restOfExp(constants, part.substring(i + 1), [], char, undefined, undefined, {}, depth + 1);
// At line 480 (template literal), also pass depth + 1:
const skip = restOfExp(constants, part.substring(i + 2), [], '{', undefined, undefined, {}, depth + 1);
}
```
Similarly, add depth tracking to `lispify` and `lispifyExpr`:
```typescript
function lispify(
constants: IConstants,
part: CodeString,
expected?: readonly string[],
lispTree?: Lisp,
topLevel = false,
depth: number = 0, // ADD depth parameter
): Lisp {
if (depth > MAX_PARSE_DEPTH) {
throw new ParseError('Expression nesting depth exceeded', part.toString());
}
// ... pass depth + 1 to recursive lispify/lispifyExpr calls ...
}
```
Related
Alias · CVE (1)
- CVE-2026-34211 SandboxJS is a JavaScript sandboxing library. vulnerability
Weakness · CWE (1)
- CWE-674 Uncontrolled Recursion weakness