GHSA-9j65-hcg3-gc64

ghsaUSosv

toml-node: Quadratic-complexity DoS via long dotted keys or table paths in toml.parse()

view advisory published 2026-10-22Licence: CC-BY-4.0

## Summary `toml.parse()` is vulnerable to a quadratic-complexity (O(n²)) denial-of-service via a single long dotted key, table header, or inline-table key. A ~100KB crafted document causes a multi-second synchronous hang; a ~200KB document crashes the Node.js process outright with an uncatchable out-of-memory error. This is a separate, unpatched code path from the previously fixed stack-overflow DoS (`GHSA-82x6-q7mm-w9cf`) — it requires no array/inline-table nesting at all, and is confirmed unaffected by the `maxDepth` option that fixed that earlier issue. ## Affected component / version - Package: `toml` (npm), repository `BinaryMuse/toml-node` - Functions: `assign()`, `deepRef()`, `setNestedKey()` in `lib/compiler.js` - Confirmed present in the audited checkout at commit `1b2ac40` (2026-07-16), one commit past the `v5.0.0` tag (a changelog-only change) — i.e. present in `v5.0.0`, the latest release. Not exhaustively verified against every prior release, but the affected functions are unrelated to `v5.0.0`'s BigInt/Temporal changes (per `CHANGELOG.md`), so the defect likely predates that release as well. ## Details `toml.parse(input, options)` (`index.js:5-9`) forwards the caller-supplied string to `parser.parse()` and then `compiler.compile()` with no length or structural validation. Inside `lib/compiler.js`, three separate functions each walk a dotted key's segments in a `for` loop and, on **every** iteration, recompute the full path-prefix string from scratch: - `assign()` (lines 69-107, the sink at line 81): `var intermediatePath = makeFullPath(keys.slice(0, i + 1));` — reached for top-level dotted-key assignments like `a.b.c = 1`. - `deepRef()` (lines 247-281, the sink at line 252): the identical pattern via `pathKey(keys.slice(0, i + 1))` — reached from table headers `[a.b.c]` and array-of-tables `[[a.b.c]]` via `setPath()`/`addTableArray()`. - `setNestedKey()` (lines 184-202, the sink at line 187): the identical pattern — reached from inline-table dotted keys like `t = { a.b.c = 1 }`. `keys.slice(0, i + 1)` allocates a new array of length `i`; `pathKey()`/`makeFullPath()` (lines 311-317) then does `.map(quoteDottedString).join(".")`, also O(i). Since this runs on every one of the `n` segments of a dotted key, total work is `O(1 + 2 + ... + n) = O(n²)`, not `O(n)`. The string is computed unconditionally on the happy path (only ever *used* if an error is about to be thrown), so there is no early exit that makes this cheap in practice. No limit exists anywhere on dotted-key segment count: `dot_ended_table_key_part+` and `inline_dot_key_part+` in `src/toml.pegjs` (lines 122, 311) are unbounded `+` repetitions, parsed iteratively rather than through the grammar's `value` rule — so the existing `MAX_DEPTH`/`maxDepth` nesting guard (`src/toml.pegjs:141-151`, enforced in the generated `lib/parser.js:1022`) does not apply to key length at all. We confirmed this directly: setting `options.maxDepth: 1` (the strictest possible value) against a 16,000-segment key produced no measurable timing difference from the unrestricted case. ## Proof of Concept ```js const toml = require('toml'); // n-segment dotted key: "k0.k1.k2. ... .k{n-1} = 1" function segKey(n) { const parts = []; for (let i = 0; i < n; i++) parts.push('k' + i); return parts.join('.') + ' = 1\n'; } // ~100.9 KB input -- takes ~6.6-6.8 seconds toml.parse(segKey(16000)); // ~200 KB input -- crashes the Node process: // "FATAL ERROR: Ineffective mark-compacts near heap limit - JavaScript heap out of memory" toml.parse(segKey(32000)); ``` Measured timings (Node v22, direct execution against this repo): | n (segments) | input size | time | |---|---|---| | 1,000 | ~4.9 KB | 34 ms | | 8,000 | ~46.9 KB | 1.6 s | | 16,000 | ~100.9 KB | 6.8 s | | 32,000 | ~200 KB | **process crash (OOM)** | Doubling `n` from 8,000→16,000 roughly quadruples runtime (~4.2x), the signature of O(n²) rather than the expected O(n) (~2x). Independently reproduced with the identical scaling signature for the table-header variant (`[k0. ... .k15999]`, exercising `deepRef()`) and the inline-table variant (`t = { k0. ... .k15999 = 1 }`, exercising `setNestedKey()`). We also generalized this into a property-based fuzzing harness (fast-check, run under Node) that automatically tests the "doubling input size should not more than roughly double parse time" invariant across 12 structural dimensions of the grammar/compiler; it independently re-flags all three code paths above on every run, with no additional dimension confirmed vulnerable after follow-up investigation. The harness is available on request. ## Impact Node.js is single-threaded — a synchronous multi-second-to-crashing `parse()` call blocks the entire event loop of the process calling it, stalling every other request/operation that process is handling, not just the attacker's own. At the upper end this is not merely a hang: an uncaught, process-terminating out-of-memory crash from a ~200KB attacker-supplied string is a full denial of service requiring no authentication — just the ability to get one string parsed by an application using this library (config upload, API request body, imported manifest, or any of this library's intended use cases). ## Suggested remediation Build the path prefix as an incrementally-extended string as the loop walks each segment (append one already-quoted segment per iteration to a running string) instead of re-slicing an array and re-joining the entire prefix from scratch on every iteration. For example, in `assign()`: ```js function assign(node) { var keys = node.key; var value = node.value; var off = node.offset; if (!Array.isArray(keys)) keys = [keys]; var reduced = reduceValueNode(value); var target = context; var pathStr = pathKey(currentPath); // computed once, not per segment for (var i = 0; i < keys.length - 1; i++) { var k = keys[i]; var quotedK = quoteDottedString(k); var intermediatePath = pathStr ? pathStr + "." + quotedK : quotedK; // O(new segment), not O(i) pathStr = intermediatePath; // ...same conditional logic as today, using intermediatePath target = target[k]; } // ... } ``` The identical treatment applies to `deepRef()` and `setNestedKey()`. As defense-in-depth, consider also capping total key-segment count or overall input size, the same way `maxDepth` already caps nesting. ## Credit Discovered and reported by SecDim Security Research secdim.com | @secdim | security@secdim.com

01

describes (1)

Related CVEs.

IDseverity exploitable
CVE-2026-114143US——
toml-node is a TOML parser for Node.js and the browser. Prior to 5.0.1, toml.parse() repeatedly slices and rejoins every path prefix in lib/compiler.js while processing long dotted…
02

references

External citations — patches, advisories, analysis.

scry

Advisories like this name the exposed products attackers go after on your perimeter. See what is exposed on your perimeter with a free passive scan.

Free scan