GitHub Advisoryadvisory · npm

GHSA-82x6-q7mm-w9cf toml-node: Uncontrolled Recursion

HIGH Published 2026-09-03 Modified 2026-09-03 Official page ↗
Aliases
CVE-2026-77465
Ecosystem
npm
Packages
toml
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-674
Reviewed
2026-09-03

Affected packages

• npm toml: < 4.2.0 → fixed in 4.2.0

Description

Summary `toml.parse()` crashes with an uncaught `RangeError: Maximum call stack size exceeded` when parsing deeply nested arrays or inline tables. The parser is generated by Peggy 5.1.0 (a PEG parser generator) as a recursive-descent parser; the value rule mutually recurses with the array and inline-table rules with no depth limit, so nesting depth equal to the input depth exhausts Node's call stack. A small payload — a bare array nested a few thousand levels deep (~5–6 KB) — reliably crashes the process on a default Node.js configuration. `toml` has ~47 million monthly downloads. --- Vulnerable Code The parser is a generated recursive-descent parser (`lib/parser.js`, header: `// @generated by Peggy 5.1.0.`). The recursion sink is the mutual recursion between the `value`, `array`, and `inline_table` rule functions — none carry a depth counter: ```javascript // lib/parser.js — peg$parsevalue() @ line 1008 function peg$parsevalue() { ... s0 = peg$parsearray(); // line 1017 ← value → array if (s0 === peg$FAILED) { s0 = peg$parseinline_table(); // line 1019 ← value → inline_table } ... } // peg$parsearray() @ line 2879 function peg$parsearray() { ... s3 = peg$parsevalue(); // line 2931 ← array element → value (back-edge) ... } // peg$parseinline_table() @ line 3066 → peg$parseinline_table_entry() @ line 3239 function peg$parseinline_table_entry() { ... s5 = peg$parsevalue(); // line 3266 ← inline-table value → value (back-edge) ... } ``` Recursion cycle for `a=[[[ … ]]]` (bare nested arrays): ``` toml.parse(src) → peg$parsevalue() # parser.js:1008 → peg$parsearray() # parser.js:1017 / 2879 → peg$parsevalue() # parser.js:2931 ← back-edge, per nested element → … # depth == input nesting → RangeError, no guard ``` Inline tables (`{arr=[ … ]}`, `{a={a= … }}`) reach the same cycle via `peg$parseinline_table` / `peg$parseinline_table_entry`. Because the parser is machine-generated, there is no hand-written function to patch; the fix belongs in the grammar (`src/toml.pegjs`) or in an input guard (see *Suggested Fix*). --- Confirmed PoC (toml 4.1.2, Node.js v24.16.0) Setup: ```bash npm install [email protected] # latest release; 4.1.1 and earlier are equally affected Docker equivalent: docker run --rm node:24 bash -c "npm i -g toml >/dev/null 2>&1; node -e ''" ``` Reproduce — save as `poc.js`, run `node poc.js`: ```javascript const toml = require('toml'); console.log('version:', require('toml/package.json').version); // 4.1.2 // Smallest reliable payload: a bare array nested 3000 levels (~6 KB) let x = '1'; for (let i = 0; i Note on the exact threshold: the precise crashing depth is not perfectly deterministic — it shifts by a few hundred levels depending on V8 JIT state, Node version, platform, and any configured `--stack-size`. This is expected for a stack-overflow condition. A payload nested a few thousand levels deep (single-digit KB) crashes reliably across runs; the PoC above (depth 3000) leaves ample margin. --- Realistic Attack Scenario ```javascript // Node.js service parsing user-supplied TOML config const express = require('express'); const toml = require('toml'); const app = express(); app.use(express.text({ type: 'application/toml', limit: '100kb' })); app.post('/config', (req, res) => { try { const config = toml.parse(req.body); // ← RangeError on ~6 KB nested payload res.json({ status: 'ok' }); } catch (e) { // toml only throws a peg$SyntaxError (e.name === 'SyntaxError', with e.line/e.column) // on malformed input. A RangeError has neither, so this guard rethrows it: if (e.line != null) return res.status(400).json({ error: e.message }); throw e; // RangeError propagates → uncaught → worker down } }); ``` An unauthenticated attacker POSTs a ~6 KB deeply nested body (well under the 100 KB limit). `toml.parse` overflows the stack and throws `RangeError`; any handler that only special-cases syntax errors rethrows it, taking down the request (and, depending on the server, the worker). > The package exports only `parse` (`Object.keys(require('toml'))` → `['parse']`); there is no `toml.SyntaxError`. Code written as `catch (e) { if (e instanceof toml.SyntaxError) … }` is itself broken (`instanceof undefined` throws), so applications generally cannot cleanly distinguish the DoS `RangeError` from a normal parse error. --- Impact Any Node.js application that calls `toml.parse()` on untrusted input is exposed to a remote, unauthenticated denial of service via a small (~5–6 KB) deeply nested payload. `toml.parse` is the package's only public API, and TOML is commonly parsed from user-supplied config/upload endpoints. With ~47 million monthly downloads and 0 existing CVEs, the exposure is broad. `RangeError` is a subclass of `Error` (not of the parser's `SyntaxError`), so it bypasses the usual "is this a parse error?" checks and propagates as an unexpected exception. --- Suggested Fix Because `lib/parser.js` is generated, the fix should be applied at the grammar level and regenerated, or guarded at the entry point: Option 1 — grammar-level depth guard (`src/toml.pegjs`), then re-run Peggy: ```javascript // In the grammar initializer: { let depth = 0; const MAX_DEPTH = 500; } // Wrap the recursive `value` rule: value = &{ if (++depth > MAX_DEPTH) { error("TOML nesting too deep"); } return true; } v:(array / inline_table / ...) { depth--; return v; } ``` Option 2 — entry-point guard in `index.js` (reject pathological input before parsing): ```javascript module.exports.parse = function (input) { // cheap structural bound before the recursive parse let depth = 0, max = 0; for (const ch of input) { if (ch === '[' || ch === '{') max = Math.max(max, ++depth); else if (ch === ']' || ch === '}') depth--; } if (max > 500) throw new Error('TOML nesting depth exceeds limit (500)'); return realParse(input); }; ``` Immediate mitigation (users, verified): bound untrusted input length and bracket-nesting depth before calling `toml.parse()`, e.g. reject payloads whose maximum `[`/`{` nesting exceeds a few hundred. A byte-length limit alone is insufficient (5 KB already crashes). --- Comparison with Related Vulnerabilities Same CWE-674 class as the recursion-DoS findings in the PyPI `toml` package (C055) and the YAML parsers (PyYAML GHSA-r9mm-j37c-pjwp, ruamel.yaml). The distinguishing detail here: the parser is generated by Peggy, so the recursion lives in `peg$parsevalue`/`peg$parsearray`/`peg$parseinline_table` and cannot be fixed by editing a hand-written function — the earlier draft of this report incorrectly showed hand-written `parseValue(tokens, index)` functions that do not exist in the package.

Related

Alias · CVE (1)

  • CVE-2026-77465 toml-node is a TOML parser for Node.js and the browser. vulnerability

Weakness · CWE (1)

  • CWE-674 Uncontrolled Recursion weakness

References