high · 7.5CVE-2026-49293Jun 26, 2026

CVE-2026-49293: js-toml CPU Exhaustion via Quadratic BigInt Parsing

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Feeding js-toml a TOML file containing a very long hex, octal, or binary integer literal causes the parser to burn CPU quadratically, letting one HTTP request block a Node.js server for tens of second

Packagejs-toml
Ecosystemnpm
Affected<= 1.1.0
Fixed in1.1.1

The problem

js-toml versions up to and including 1.1.0 parse radix-prefixed integer literals (0x, 0o, 0b) with a hand-written loop that multiplies a growing BigInt accumulator once per digit. After i iterations the accumulator has O(i) internal limbs, so each multiply costs O(i) and the full loop over n digits costs O(n^2).

The lexer places no upper bound on literal length, so a single TOML document can supply hundreds of thousands of digits. On a modern laptop, a 500 kB hex literal blocks the event loop for roughly 40 seconds. Doubling the literal length quadruples the work. Any code path that calls load() on attacker-supplied TOML is exposed.

Proof of concept

javascript
// install: npm install js-toml@1.1.0
import { load } from 'js-toml';

// ~500 kB hex literal -- pins one CPU core for ~40 s on Node v22+ (Apple M-series)
const hexDigits = 'f'.repeat(500_000);
const maliciousToml = `x = 0x${hexDigits}`;

console.time('parse');
load(maliciousToml);   // blocks event loop; returns a BigInt of 2,000,000 bits
console.timeEnd('parse');
// Output: parse: ~40388ms

// Octal and binary share the same code path:
// load(`x = 0o${'7'.repeat(100_000)}`);  // ~633 ms
// load(`x = 0b${'1'.repeat(100_000)}`);  // ~197 ms

The root cause is in src/load/tokens/NonDecimalInteger.ts. The parseBigInt helper does result = result * BigInt(radix) + BigInt(digit) for every character. Each BigInt multiply is proportional to the current size of result, making the total work O(n^2).

The lexer regex (0x<hexDigit>(<hexDigit>|_<hexDigit>)*) has no length limit, so there is nothing to stop an arbitrarily long literal from reaching this loop. The decimal integer path uses the native BigInt(intString) constructor, which is O(n) in V8, and parses the same data thousands of times faster.

The patch (commit 1abcb31) deletes parseBigInt entirely and replaces the fallback with BigInt(intString), where intString already carries the 0x/0o/0b prefix. The fix also adds a MAX_RADIX_LITERAL_LENGTH guard that throws SyntaxParseError for literals longer than 1000 digits, matching the constraint approach used by jackson-core StreamReadConstraints.

CWEs: CWE-407 (Inefficient Algorithmic Complexity), CWE-400 (Uncontrolled Resource Consumption).

The fix

Upgrade to js-toml 1.1.1. The fix replaces the O(n^2) parseBigInt loop with a single call to the native BigInt() constructor and adds a 1000-digit length cap on radix-prefixed literals. If you cannot upgrade immediately, reject or size-limit any TOML input before passing it to load().

Reported by tonghuaroot.

References: [1][2][3][4][5]