CVE-2026-59879: Immutable Denial of Service Vulnerability

HIGH
Advisory Identifier: GHSA-v56q-mh7h-f735
CVSS 7.5
CWE-190 CWE-400 CWE-1284 CWE-835
Published
Advisory Summary

CVE-2026-59879 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-190: Integer Overflow or Wraparound) affecting Immutable libraries in the npm ecosystem. The issue involves immutable.js `list` 32-bit trie overflow → unrecoverable dos. Remediation is available by upgrading affected packages to version 4.3.9, 5.1.8, 3.8.4.

CVE-2026-59879 at a Glance

CVE Identifier CVE-2026-59879
GHSA Identifier GHSA-v56q-mh7h-f735
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-190: Integer Overflow or Wraparound
Affected Product Immutable
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-59879

To remediate this vulnerability, upgrade all affected Immutable dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

immutable fixed: 4.3.9
npm install immutable@4.3.9
immutable fixed: 5.1.8
npm install immutable@5.1.8
immutable fixed: 3.8.4
npm install immutable@3.8.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls immutable

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-59879, inspect your project dependencies using npm:

npm ls immutable

Analyze the terminal output to locate any version of Immutable in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.3.9), your application is affected.

Note: The npm ls immutable command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-190: Integer Overflow or Wraparound, where the product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. this occurs when an integer value is incremented to a value that is too large to store in the associated representation. when this occurs, the value may become a very small or negative number. In Immutable, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `List#set`, `List#setSize`, `List#setIn`, `List#updateIn` (and the functional `set` / `setIn` / `updateIn`) mishandle an index or size in the range `[2 ** 30, 2 ** 31)`: - On an **empty** `List` the operation enters an **uncatchable infinite loop** (a tight CPU spin; a surrounding `try/catch` never regains control). Only killing the worker recovers it. - On a **populated** `List` (≥ 32 elements — i.e. any array of ≥ 32 items turned into a `List` by `fromJS`) the loop allocates without bound → heap exhaustion → the **process aborts** (`SIGABRT`, exit `134`, or kernel OOM-kill `137`). A real crash, not a recoverable error. The index may be a **numeric string**, so it can come straight from a request body, URL, or key-path. A single small unauthenticated request is enough. There is also a companion **silent data-corruption** issue in `setSize`: ```js List([1, 2, 3]).setSize(2 ** 31); // before fix => size 0 (silently cleared) List([1, 2, 3]).setSize(2 ** 32 + 5); // before fix => size 5 (huge value wraps to 5) ``` ## Impact Availability only. A reachable configuration is any endpoint that routes untrusted input into a `List` index or a `setIn`/`updateIn` key-path — which the extremely common `state = fromJS(body); state.setIn(userPath, value)` pattern does (config stores, document/collection editors, redux-immutable reducers, JSON-Patch endpoints, etc.). No confidentiality or integrity impact, no RCE. The companion `setSize` bug can silently corrupt application state (wrong size) without crashing. ## Reproduction (immutable 5.1.7) ```ts import { fromJS, List } from 'immutable'; // 1) Populated List: OOM -> process abort (SIGABRT, exit 134) within ~2s fromJS({ items: new Array(64).fill(0) }).setIn(['items', '1073741824'], 'x'); // 2) Empty List: hangs forever, uncatchable List().set(2 ** 30, 'x'); // 3) Silent truncation List([1, 2, 3]).setSize(2 ** 31); // => size 0 List([1, 2, 3]).setSize(2 ** 32 + 5); // => size 5 ``` A remote 43-byte HTTP request (`{"path":["items","1073741824"],"value":"x"}`) is sufficient to abort a worker that applies it via `state = state.setIn(path, value)`. Any index in `[2 ** 30, 2 ** 31)` works (`1073741824`, `2000000000`, …). An index in `[2 ** 31, 2 ** 32)` does not crash — it silently wraps (clearing the List) via the same root cause. ## Root cause `List` stores its values in a 32-wide trie (`SHIFT = 5`, so each level addresses 5 more bits) and uses **signed 32-bit bitwise arithmetic** throughout `setListBounds()` (`src/List.js`): 1. **Infinite loop (the hang / OOM).** The level-raising loop ```js while (newTailOffset >= 1 << (newLevel + SHIFT)) { newRoot = new VNode( newRoot && newRoot.array.length ? [newRoot] : [], owner ); newLevel += SHIFT; } ``` relies on `1 << (newLevel + SHIFT)`. A JavaScript shift count is taken **mod 32**, so once `newLevel + SHIFT` reaches `31` the term goes **negative** (`1 << 31 === -2147483648`) and at `32` wraps to `1` (`1 << 35 === 8`). The comparison then stays `true` forever and the loop never terminates. On a populated `List`, each iteration retains a new `VNode` (`[newRoot]`), so the heap fills and V8 aborts; on an empty `List` it spins on CPU without allocating. 2. **Silent wraparound (the `setSize` corruption).** The `begin |= 0` / `end |= 0` coercion (`ToInt32`) silently wraps large finite values (`(2 ** 31) | 0 === -2147483648`, `(2 ** 32 + 5) | 0 === 5`), producing a wrong resulting size instead of an error. The threshold is `2 ** 30`: that is the largest size for which `1 << (newLevel + SHIFT)` stays a valid positive 32-bit integer throughout the loops (`newLevel + SHIFT` stays ≤ 30). ## Remediation The fix is contained to `setListBounds()` in `src/List.js`: 1. **Validate up front, before the lossy `| 0` coercion.** Compute the intended origin and capacity in full precision and throw a clear, catchable `RangeError` when they exceed the addressable range (`MAX_LIST_SIZE = 2 ** 30`). `Infinity`/`NaN` are left to the existing `| 0 → 0` behaviour (so `setSize(Infinity)` stays `0` and `slice(0, Infinity)` still means "to the end"). 2. **Stop the shift from wrapping.** Replace `1 << exp` in the level-raising loops with a helper that uses the cheap bitwise shift while it is exact (`exp ≤ 30`, the common path including every `push`/`setSize`/`slice`) and falls back to the non-wrapping `2 ** exp` only for the rare deep trees reached when a negative origin (`unshift` / negative index) is normalized to a large positive capacity (`exp` can reach 35 there, where `1 << 35` would wrap to 8). This turns every hang, the misleading `"Maximum call stack size exceeded"`, the OOM/`SIGABRT`, and the silent `setSize` truncation into one descriptive `RangeError`, preserves all behaviour for sizes `< 2 ** 30`, and keeps the hot `push` path on the fast bitwise shift (the `2 ** exp` branch is never reached by non-negative operations). ### Is the new limit a breaking change? No working code is affected. A `List` could never actually hold `≥ 2 ** 30` values before — the attempt hung, crashed, or silently corrupted the size. The limit was already implicit in the 32-bit trie; the fix only makes it explicit and catchable, mirroring native JS arrays (`new Array(2 ** 32)` → `RangeError: Invalid array length`). The single observable behaviour change is that `setSize(hugeValue)`, which used to return a silently wrong size, now throws. `2 ** 30` ≈ 1.07 billion entries (~8 GB of pointers alone), far beyond any practical use. ## Mitigations (for users who cannot upgrade immediately) - Validate/clamp any externally supplied `List` index or `setIn`/`updateIn` key-path segment against a sane maximum before passing it to immutable. - Reject numeric path segments `≥ 2 ** 30`. - Run request handling in a worker that can be restarted, and cap the heap (`--max-old-space-size`) so an abort is contained.

CVE-2026-59879 Affected Immutable Versions

3 component(s)
Fixed Versions:
4.3.95.1.83.8.4
Product / Package Ecosystem Affected Versions Fixed Version
immutable
npm
≥ 4.0.0-rc.1
< 4.3.9
4.3.9
immutable
npm
≥ 5.0.0-beta.1
< 5.1.8
5.1.8
immutable
npm
< 3.8.4
3.8.4

References

Frequently Asked Questions

What is CVE-2026-59879?
CVE-2026-59879 is a security vulnerability in Immutable involving Immutable.js `List` 32-bit trie overflow → unrecoverable DoS under CWE-190: Integer Overflow or Wraparound.
Which Immutable versions are affected?
Affected versions include releases prior to 4.3.9, 5.1.8, 3.8.4 across affected packages: immutable.
What version fixes CVE-2026-59879?
Upgrading to 4.3.9, 5.1.8, 3.8.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is immutable affected?
Yes, immutable is affected in releases prior to 4.3.9, 5.1.8, 3.8.4. Upgrade your dependency to 4.3.9, 5.1.8, 3.8.4 or later.
What is the CVSS score of CVE-2026-59879?
CVE-2026-59879 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-59879?
CVE-2026-59879 is classified under CWE-190: Integer Overflow or Wraparound (The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.).
How can I check npm dependencies for CVE-2026-59879?
Run the dependency tree command: 'npm ls immutable'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-v56q-mh7h-f735",
  "modified" : "2026-09-16T18:47:10Z",
  "published" : "2026-07-21T18:36:27Z",
  "aliases" : [ "CVE-2026-59879" ],
  "summary" : "Immutable.js `List` 32-bit trie overflow → unrecoverable DoS",
  "details" : "## Summary\n\n`List#set`, `List#setSize`, `List#setIn`, `List#updateIn` (and the functional `set` / `setIn` / `updateIn`) mishandle an index or size in the range `[2 ** 30, 2 ** 31)`:\n\n- On an **empty** `List` the operation enters an **uncatchable infinite loop** (a tight CPU spin; a surrounding `try/catch` never regains control). Only killing the worker recovers it.\n- On a **populated** `List` (≥ 32 elements — i.e. any array of ≥ 32 items turned into a `List` by `fromJS`) the loop allocates without bound → heap exhaustion → the **process aborts** (`SIGABRT`, exit `134`, or kernel OOM-kill `137`). A real crash, not a recoverable error.\n\nThe index may be a **numeric string**, so it can come straight from a request body, URL, or key-path. A single small unauthenticated request is enough.\n\nThere is also a companion **silent data-corruption** issue in `setSize`:\n\n```js\nList([1, 2, 3]).setSize(2 ** 31); // before fix => size 0  (silently cleared)\nList([1, 2, 3]).setSize(2 ** 32 + 5); // before fix => size 5  (huge value wraps to 5)\n```\n\n## Impact\n\nAvailability only. A reachable configuration is any endpoint that routes untrusted input into a `List` index or a `setIn`/`updateIn` key-path — which the extremely common `state = fromJS(body); state.setIn(userPath, value)` pattern does (config stores, document/collection editors, redux-immutable reducers, JSON-Patch endpoints, etc.).\n\nNo confidentiality or integrity impact, no RCE. The companion `setSize` bug can silently corrupt application state (wrong size) without crashing.\n\n## Reproduction (immutable 5.1.7)\n\n```ts\nimport { fromJS, List } from 'immutable';\n\n// 1) Populated List: OOM -> process abort (SIGABRT, exit 134) within ~2s\nfromJS({ items: new Array(64).fill(0) }).setIn(['items', '1073741824'], 'x');\n\n// 2) Empty List: hangs forever, uncatchable\nList().set(2 ** 30, 'x');\n\n// 3) Silent truncation\nList([1, 2, 3]).setSize(2 ** 31); // => size 0\nList([1, 2, 3]).setSize(2 ** 32 + 5); // => size 5\n```\n\nA remote 43-byte HTTP request (`{\"path\":[\"items\",\"1073741824\"],\"value\":\"x\"}`) is sufficient to abort a worker that applies it via `state = state.setIn(path, value)`.\n\nAny index in `[2 ** 30, 2 ** 31)` works (`1073741824`, `2000000000`, …). An index in `[2 ** 31, 2 ** 32)` does not crash — it silently wraps (clearing the List) via the same root cause.\n\n## Root cause\n\n`List` stores its values in a 32-wide trie (`SHIFT = 5`, so each level addresses 5 more bits) and uses **signed 32-bit bitwise arithmetic** throughout `setListBounds()` (`src/List.js`):\n\n1. **Infinite loop (the hang / OOM).** The level-raising loop\n\n```js\nwhile (newTailOffset >= 1 << (newLevel + SHIFT)) {\n  newRoot = new VNode(\n    newRoot && newRoot.array.length ? [newRoot] : [],\n    owner\n  );\n  newLevel += SHIFT;\n}\n```\n\nrelies on `1 << (newLevel + SHIFT)`. A JavaScript shift count is taken **mod 32**, so once `newLevel + SHIFT` reaches `31` the term goes **negative** (`1 << 31 === -2147483648`) and at `32` wraps to `1` (`1 << 35 === 8`). The comparison then stays `true` forever and the loop never terminates. On a populated `List`, each iteration retains a new `VNode` (`[newRoot]`), so the heap fills and V8 aborts; on an empty `List` it spins on CPU without allocating.\n\n2. **Silent wraparound (the `setSize` corruption).** The `begin |= 0` / `end |= 0` coercion (`ToInt32`) silently wraps large finite values (`(2 ** 31) | 0 === -2147483648`, `(2 ** 32 + 5) | 0 === 5`), producing a wrong resulting size instead of an error.\n\nThe threshold is `2 ** 30`: that is the largest size for which `1 << (newLevel + SHIFT)` stays a valid positive 32-bit integer throughout the loops (`newLevel + SHIFT` stays ≤ 30).\n\n## Remediation\n\nThe fix is contained to `setListBounds()` in `src/List.js`:\n\n1. **Validate up front, before the lossy `| 0` coercion.** Compute the intended origin and capacity in full precision and throw a clear, catchable `RangeError` when they exceed the addressable range (`MAX_LIST_SIZE = 2 ** 30`). `Infinity`/`NaN` are left to the existing `| 0 → 0` behaviour (so `setSize(Infinity)` stays `0` and `slice(0, Infinity)` still means \"to the end\").\n\n2. **Stop the shift from wrapping.** Replace `1 << exp` in the level-raising loops with a helper that uses the cheap bitwise shift while it is exact (`exp ≤ 30`, the common path including every `push`/`setSize`/`slice`) and falls back to the non-wrapping `2 ** exp` only for the rare deep trees reached when a negative origin (`unshift` / negative index) is normalized to a large positive capacity (`exp` can reach 35 there, where `1 << 35` would wrap to 8).\n\nThis turns every hang, the misleading `\"Maximum call stack size exceeded\"`, the OOM/`SIGABRT`, and the silent `setSize` truncation into one descriptive `RangeError`, preserves all behaviour for sizes `< 2 ** 30`, and keeps the hot `push` path on the fast bitwise shift (the `2 ** exp` branch is never reached by non-negative operations).\n\n### Is the new limit a breaking change?\n\nNo working code is affected. A `List` could never actually hold `≥ 2 ** 30` values before — the attempt hung, crashed, or silently corrupted the size. The limit was already implicit in the 32-bit trie; the fix only makes it explicit and catchable, mirroring native JS arrays (`new Array(2 ** 32)` → `RangeError: Invalid array length`). The single observable behaviour change is that `setSize(hugeValue)`, which used to return a silently wrong size, now throws. `2 ** 30` ≈ 1.07 billion entries (~8 GB of pointers alone), far beyond any practical use.\n\n## Mitigations (for users who cannot upgrade immediately)\n\n- Validate/clamp any externally supplied `List` index or `setIn`/`updateIn` key-path segment against a sane maximum before passing it to immutable.\n- Reject numeric path segments `≥ 2 ** 30`.\n- Run request handling in a worker that can be restarted, and cap the heap (`--max-old-space-size`) so an abort is contained.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  }, {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "immutable"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0-rc.1"
      }, {
        "fixed" : "4.3.9"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "immutable"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "5.0.0-beta.1"
      }, {
        "fixed" : "5.1.8"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "immutable"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.8.4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/security/advisories/GHSA-v56q-mh7h-f735"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-59879"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/commit/a1a1ee412dcaa380ab325196283d06594ffe4b84"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/commit/f0bc997d8eb9886aff2236635aa210a95a04304a"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/immutable-js/immutable-js"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/releases/tag/v3.8.4"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/releases/tag/v4.3.9"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/immutable-js/immutable-js/releases/tag/v5.1.8"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1284", "CWE-190", "CWE-400", "CWE-835" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T18:36:27Z",
    "nvd_published_at" : "2026-07-08T17:17:26Z"
  }
}