---
id: "CVE-2026-114143"
kind: "vulnerability"
aliases: ["CVE-2026-114143", "GHSA-9j65-hcg3-gc64"]
title: "toml-node: Quadratic-complexity DoS via long dotted keys or table paths in toml.parse()"
severity: "high"
cvss: 8.7
epss: 0.0029100000392645597
exploited: false
status: "open"
category: "package"
products: [{"name": "toml", "fixed": ["5.0.1"], "affected": "<= 5.0.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["config-upload", "cwe-407", "node.js", "npm", "package", "self-hosted", "toml"]
not_affected_if: ["default-config", "managed-hosting", "no-untrusted-toml-input"]
published_at: "2026-10-21T21:17:56+00:00"
first_seen_at: "2026-10-23T19:11:31+00:00"
updated_at: "2026-10-23T21:57:29+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-9j65-hcg3-gc64", "first_seen_at": "2026-10-23T19:11:31+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-114143", "first_seen_at": "2026-10-23T21:57:29+00:00"}]
---

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

The npm package toml (toml-node) has a quadratic-time parsing path: a single very long dotted key, table header or inline-table key makes toml.parse() block the Node.js event loop for seconds, and about 200KB of input crashes the process with an out-of-memory error. An attacker who can get one crafted string parsed by an application using this library can cause a denial of service without authentication.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:toml affected <= 5.0.0 fixed 5.0.1

## How to check

Run npm ls toml to find the installed version; affected versions are 5.0.0 and below.
Search the codebase for require('toml') or import of 'toml' and note where parse() receives untrusted input such as uploads, request bodies or imported manifests.

## Fix

Upgrade the toml package to 5.0.1 or later.
Until upgraded, limit the size of TOML input accepted from untrusted sources before calling toml.parse().

## Timeline

- 2026-10-23T19:11:31+00:00 ghsa https://github.com/advisories/GHSA-9j65-hcg3-gc64
- 2026-10-23T21:57:29+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-114143

## From the sources

## 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

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
