Back to Blog
high SEVERITY4 min read

nanoid 3.3.11 Integer Overflow: Predictable ID Generation

An integer overflow in nanoid 3.3.11's internal randomness generation causes the library to fall back to predictable ID sequences, undermining the cryptographic guarantees of its supposedly unguessable identifiers. The fix upgrades the dependency tree to patched versions 3.3.12 or 5.1.11.

O
By Orbis AppSec
•Published September 28, 2026•Reviewed September 28, 2026

Answer Summary

nanoid versions 3.3.11 and earlier contain an integer overflow in their random byte generation that causes ID collisions and predictability. An attacker who observes multiple IDs can predict future values, bypassing rate limits, session protections, or URL-guessing defenses that rely on nanoid's unguessability. The vulnerability is fixed by upgrading to nanoid 3.3.12 or 5.1.11, which correct the overflow handling in the internal `randomFillSync` wrapper. The CWE is unknown.

Vulnerability at a Glance

cweN/A
fixUpgrade to nanoid 3.3.12 or 5.1.11 with corrected pool size arithmetic
riskSession fixation, rate limit bypass, unauthorized resource access via ID prediction
languageJavaScript (Node.js)
root causeInteger overflow in random byte pool calculation causes fallback to non-random sequence
vulnerabilityInteger Overflow leading to Predictable ID Generation

Affected Versions

Affected <= 3.3.11
Fixed in 3.3.12, 5.1.11
Ecosystem npm
CVE / GHSA CVE-2026-73086 / not assigned
CWE unknown

The Vulnerability Explained

The nanoid library promises "secure, URL-friendly, unique" identifiers by defaulting to 21 characters from a 64-symbol alphabet, yielding ~128 bits of entropy. This strength relies on a uniform random distribution from Node.js's crypto.randomFillSync. CVE-2026-73086 breaks that promise through an integer overflow in how nanoid calculates its internal randomness pool size.

In affected versions, nanoid maintains a pool of random bytes to amortize the cost of system calls. When the pool needs replenishment, it calculates how many bytes to request. An integer overflow in this calculation—specifically when handling large ID sizes or specific call patterns—causes the pool size to wrap to zero or a negative value. Node.js's randomFillSync then receives an invalid size, triggering an error path that nanoid's internal error handler mishandles.

Rather than crashing or retrying, the affected code falls back to a deterministic sequence based on uninitialized or stale pool contents. The result: nanoid() begins emitting predictable, sequential IDs instead of cryptographically random ones.

Consider an application using nanoid for session tokens:

import { nanoid } from 'nanoid';

// In 3.3.11, this may return predictable values after pool exhaustion
const sessionId = nanoid(); // "V1StGXR8_Z5jdHi6B-myT" — but possibly guessable

An attacker who observes a few session IDs—perhaps through legitimate account creation or leaked logs—can extrapolate the internal state and predict future valid sessions. This bypasses rate limiting, enables session fixation attacks, or allows direct access to unlisted resources protected only by URL secrecy.

The attack scales with observation: each leaked ID reduces the search space for the internal PRNG state. In high-throughput services where nanoid is called thousands of times per second, pool exhaustion occurs rapidly, accelerating the shift to predictable output.

The Fix

The patched versions correct the pool size arithmetic to prevent integer overflow and add explicit bounds checking before calling randomFillSync. When the pool calculation would overflow, the code now clamps to a safe maximum and ensures fresh randomness is always mixed into the output.

The remediation in this repository upgrades nanoid through its dependency tree. The package-lock.json change shows:

-      "version": "3.3.11",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "version": "5.1.16",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
       "bin": {
-        "nanoid": "bin/nanoid.cjs"
+        "nanoid": "bin/nanoid.js"
       },
       "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+        "node": "^18 || >=20"
       }

Note the ecosystem changes: version 5.x switches from CommonJS to ES modules (nanoid.cjs → nanoid.js) and drops support for Node.js versions below 18. The 3.3.12 patch maintains backward compatibility for projects that cannot migrate to 5.x immediately.

The engines field change is significant: it reflects that the patched code uses modern Node.js crypto APIs unavailable in older runtimes. Projects pinned to Node.js 14 or 16 must evaluate their upgrade path—either to Node.js 18+ with nanoid 5.x, or to the 3.3.12 backport with continued legacy support.

Key Takeaways

  • Integer overflows in resource sizing are security-critical: A calculation that seems purely internal—how many random bytes to request—became an exploitable vulnerability when overflow changed program behavior rather than crashing safely.

  • Fallback paths in crypto code need scrutiny: nanoid's error handling for randomFillSync failures created a worse outcome than the error itself. Security-sensitive code should fail closed, not fall back to weaker mechanisms.

  • Patch version upgrades in transitive dependencies matter: The vulnerable code may be deep in your dependency tree, invoked by packages you don't directly control. Automated scanning for CVEs in lockfiles catches what manual package.json review misses.

  • ES module migration is becoming a security requirement: The 5.x line's engine bump reflects a broader ecosystem trend where security patches increasingly require modern runtime features. Technical debt in Node.js version support becomes security debt.

  • Observable identifiers leak state: Any ID generation scheme that might become predictable transforms from an asset to a liability. Logging, caching, and error reporting that expose these IDs accelerate attacks that depend on state observation.

How Orbis AppSec Detected This

Source: The nanoid package version 3.3.11 in the dependency tree, transitively included through package-lock.json.

Sink: The nanoid() function and its internal randomFillSync wrapper, where the pool size calculation overflows.

Missing control: No bounds validation on the pool size arithmetic before passing to the Node.js crypto API; insufficient error handling when randomFillSync receives invalid parameters.

CWE: unknown (integer overflow without assigned CWE)

Fix: Upgrade to nanoid 3.3.12 or 5.1.11, which correct the pool size calculation and enforce safe fallback behavior.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2026-73086 demonstrates how a single integer overflow in auxiliary code—the byte pool sizing logic—can undermine the core security guarantee of a widely trusted library. nanoid's entire value proposition rests on unpredictability; when that fails, every system relying on nanoid for session tokens, password reset URLs, or unlisted resource addresses becomes vulnerable to enumeration attacks.

The fix is straightforward: upgrade to 3.3.12 or 5.1.11. But the lesson is broader: cryptographic libraries need defense in depth at every layer, including the seemingly mundane arithmetic that determines how much randomness to request.

Prevention and further reading

Frequently Asked Questions

Does the nanoid 5.1.11 fix change the `bin/nanoid.cjs` entry point to `bin/nanoid.js`?

Yes. The 5.x line switches from CommonJS (`nanoid.cjs`) to ES modules (`nanoid.js`), and also raises the minimum Node.js version from `^10 || ^12 || ^13.7 || ^14 || >=15.0.1` to `^18 || >=20`.

Why does the package-lock.json diff show `libc` array removals alongside the nanoid upgrade?

Those removals are lockfile format changes from a newer npm version and are unrelated to the CVE. The security-relevant change is the version bump from `3.3.11` to `3.3.12` or `5.1.16` (patched to 5.1.11).

If my code doesn't directly call nanoid, am I still affected by CVE-2026-73086?

Possibly. The Orbis scan detected nanoid 3.3.11 in your dependency tree, but could not verify runtime reachability. Any transitive dependency that imports `nanoid` and calls its default export or `nanoid()` function would trigger the vulnerable code path.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

high

generateUUID() Ditches MD5 for SHA-256 to Fix CWE-328

The `generateUUID()` helper built request-identifying UUIDs by hashing an input string with MD5, a cryptographically broken algorithm susceptible to collisions. The fix swaps the hash function for SHA-256, reducing the chance that two different inputs produce the same generated identifier.

high

API_URL Defaults to HTTP Without HTTPS Enforcement

An API client defaults to unencrypted HTTP connections, leaving all API communications—including sensitive project data—vulnerable to interception. Although an `upgradeToHttps()` helper function existed, it was applied inconsistently across the codebase. The fix ensures HTTPS enforcement is applied uniformly before every HTTP request.

high

HTTP Client `danger_accept_invalid_certs` Permitted MITM Credential

The HTTP client's `validate_certs` parameter allowed disabling TLS certificate validation through `danger_accept_invalid_certs(true)`, exposing Basic Auth credentials to interception. The fix replaces this dangerous capability with a hard error, forcing developers to use proper certificate management instead.

critical

JWT Authentication Disabled Signature Validation in

A critical misconfiguration in JWT authentication explicitly disabled signature validation, allowing attackers to forge valid tokens with arbitrary claims and bypass authentication entirely. The fix re-enables signature validation on all incoming bearer tokens, restoring the security boundary of the authentication layer.

critical

`requests.get()`/`delete()`/`post()` with `verify=False` in Release

A critical security vulnerability in a release automation script disabled SSL certificate verification on every HTTPS request to GitHub's API. By passing `verify=False` to `requests.get()`, `requests.delete()`, and `requests.post()`, the script exposed OAuth tokens and release binaries to man-in-the-middle attacks on any network the script ran from.

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li