toml-node: Quadratic-complexity DoS via long dotted keys or table paths in toml.parse()
CVE-2026-114143 · GHSA-9j65-hcg3-gc64 · published 22 Oct 2026
How serious is it
- Severity: high, CVSS score 8.7 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114143?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
toml(npm) <= 5.0.0
How to check
- Run
npm ls tomlin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
tomlto 5.0.1 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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 = pathSt…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/BinaryMuse/toml-node/security/advisories/GHSA-9j65-hcg3-gc64
- nvd.nist.gov/vuln/detail/CVE-2026-114143
- github.com/BinaryMuse/toml-node/commit/609c873fffea0a27ebe5b4dd098eed2b3daf3606
- github.com/BinaryMuse/toml-node/releases/tag/v5.0.1
- github.com/advisories/GHSA-9j65-hcg3-gc64
- github.com/BinaryMuse/toml-node
Merged from GitHub Advisory Database, OSV. Last updated 25 Oct 2026. All vulnerabilities · RSS · JSON