Disclosed on September 01, 2026 (updated September 01, 2026)
PGV-2664639 is a category 3 vulnerabilty that affects nanoid, versions < 3.3.12, ≥ 4.0.0 & < 5.1.11
The risk assessment shows that this vulnerability is exlpoited by a external attacker. An unauthorized external actor who attempts to exploit this vulnerability without legitimate access.
The impact is contained to the application. Exploitation remains confined to the application and cannot affect the host environment or external systems.
The threat damage is caused by a data breach. Exploitation can result in full access to data within the system.is caused by data tampering. Exploitation can result in modification of any data (authorized or not) within the system.
An integer overflow in nanoid(size) permanently corrupts the process-wide CSPRNG pool, causing all subsequent ID generation to return the deterministic string "uuuuuuuuuuuuuuuuuuuuu". Any application that passes user-influenced values to the size parameter loses all randomness guarantees for session tokens, CSRF tokens, and unique identifiers until process restart.
nanoid() at index.js:101 coerces the size parameter with size |= 0, which converts it to a signed 32-bit integer. When size >= 2^31 (e.g., 2147483648), this wraps to -2147483648.
The negative value is passed to fillPool() (index.js:15):
function fillPool(bytes) {
if (!pool || pool.length < bytes) { // false: pool exists, -2B < pool.length
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER)
crypto.getRandomValues(pool)
poolOffset = 0
} else if (poolOffset + bytes > pool.length) { // false: poolOffset + (-2B) < pool.length
crypto.getRandomValues(pool)
poolOffset = 0
}
poolOffset += bytes // poolOffset += -2147483648 → deeply negative
}
Neither branch triggers, so the pool is never refreshed. poolOffset becomes ~-2.1 billion.
Subsequent nanoid() calls execute:
for (let i = poolOffset - size; i < poolOffset; i++) {
id += scopedUrlAlphabet[pool[i] & 63]
}
pool[negative_index] returns undefined. undefined & 63 evaluates to 0. urlAlphabet[0] is 'u'. Every ID becomes "uuuuuuuuuuuuuuuuuuuuu".
The corruption is persistent — it affects all subsequent calls in the process until ~100 million calls eventually wrap poolOffset back to positive, or the process restarts.
import { nanoid } from 'nanoid'
// Step 1: Normal operation
console.log(nanoid()) // e.g., "V1StGXR8_Z5jdHi6B-myT"
// Step 2: Trigger overflow (e.g., from an API parameter)
try { nanoid(2147483648) } catch(e) {}
// Step 3: All subsequent IDs are deterministic
console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"
console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"
console.log(nanoid()) // "uuuuuuuuuuuuuuuuuuuuu"
// ... forever, process-wide
Run with: node --experimental-vm-modules poc.mjs
Attack scenario: Any API endpoint that accepts a user-controlled length/size parameter (URL shortener slug length, configurable token size, etc.) and passes it to nanoid(userInput).
Complete loss of ID unpredictability and uniqueness, process-wide, from a single request.
nanoid in the same processsize parameter without validation| Network Exposure | External Accessable from the public internet |
| Access Interface | WebBrowser Primarily web-based applications |
| Service Outage | Disruptive Operations would be impacted |
| Data Breach | Disruptive Operations would be impacted |
| Data Tampering | Disruptive Operations would be impacted |
| Customize | |