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
randomFillSyncfailures 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.jsonreview 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.