How Denial of Service via Infinite Loop Happens in JavaScript and How to Fix It
The Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-67213 |
| Severity | HIGH |
| Library | nanoid (< 3.3.18, < 5.1.6) |
| CWE | CWE-835: Loop with Unreachable Exit Condition |
| Component | remotion-composer/package-lock.json |
| Fix | Upgrade to nanoid 3.3.18 + package.json overrides |
Introduction
The remotion-composer package uses nanoid as part of its dependency tree — a library trusted by millions of JavaScript projects to generate compact, collision-resistant random IDs. But nanoid versions before 3.3.18 (v3) and 5.1.6 (v5) carry a subtle and dangerous flaw: when customAlphabet is called with certain input configurations, the internal random byte generation loop can spin forever, locking the Node.js event loop and taking the entire application offline.
This is exactly the kind of vulnerability that doesn't announce itself with a crash or an error message. The process simply stops responding — no stack trace, no exception, just silence. For developers relying on nanoid for session tokens, short link generation, or any ID-heavy workload in a server environment, this represents a real availability risk.
Trivy's CVE scanner flagged the vulnerable version (3.3.16) locked in remotion-composer/package-lock.json, triggering this remediation.
The Vulnerability Explained
What nanoid's customAlphabet Does
nanoid's primary appeal is its flexibility. Beyond the default URL-safe alphabet, it exposes a customAlphabet function that lets developers define their own character sets for ID generation:
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('ABCDEFG', 10);
nanoid(); // → 'BGCAFEDCBA'
Internally, nanoid uses a rejection-sampling algorithm to ensure uniform distribution across the custom alphabet. It generates random bytes, maps them to characters, and discards any bytes that fall outside the valid range — repeating the process until it has collected enough valid characters.
The Infinite Loop Flaw
The vulnerability (CVE-2026-67213) lives in this rejection-sampling loop. In versions before 3.3.18/5.1.6, certain alphabet configurations — particularly very small alphabets or alphabets whose size creates an unfavorable ratio with the random byte pool — can cause the rejection rate to approach 100%. Every generated byte gets discarded, the loop never accumulates enough valid characters, and the function never returns.
Here is the conceptual structure of the vulnerable loop:
// Simplified pseudocode of the vulnerable pattern in nanoid < 3.3.18
function customAlphabet(alphabet, defaultSize = 21) {
return function nanoid(size = defaultSize) {
let id = '';
while (id.length < size) {
const bytes = random(size); // generate random bytes
for (let i = bytes.length - 1; i >= 0; i--) {
// If the byte doesn't map cleanly to the alphabet size,
// it is discarded — but in edge cases, ALL bytes are discarded
const byte = bytes[i] & mask;
if (byte < alphabet.length) {
id += alphabet[byte];
}
// No escape hatch if every byte fails this condition
}
// Loop continues forever if no bytes ever pass
}
return id;
};
}
The critical missing control: there is no upper bound on the number of iterations. If the mask and alphabet.length combination results in every sampled byte being rejected, the while (id.length < size) condition is never satisfied, and the function loops indefinitely.
The Vulnerable Dependency in package-lock.json
The remotion-composer/package-lock.json had nanoid pinned at version 3.3.16:
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="
}
This locked version carries the unpatched loop logic, making any code path that calls customAlphabet with adversarial or edge-case input a potential DoS vector.
Attack Scenario
Consider a Remotion-based rendering service that accepts user-supplied configuration for output file naming, using nanoid with a custom alphabet derived from user input:
// Hypothetical usage pattern in a rendering pipeline
import { customAlphabet } from 'nanoid';
app.post('/render', (req, res) => {
const { allowedChars } = req.body; // user-controlled!
const generateId = customAlphabet(allowedChars, 16);
const jobId = generateId(); // ← HANGS if allowedChars triggers the bug
startRenderJob(jobId);
res.json({ jobId });
});
An attacker who can influence the alphabet string — even indirectly through configuration files, API parameters, or environment variables — can send a single request that permanently stalls the Node.js event loop. Because Node.js is single-threaded, one hung request blocks all subsequent requests, achieving a full Denial of Service with minimal effort.
Even without direct user control, an accidental misconfiguration in a build pipeline or CI script could trigger the same outcome.
The Fix
Two-Part Remediation
The fix required changes to both package-lock.json and package.json — and understanding why both were necessary reveals an important lesson about Node.js dependency management.
Part 1: Upgrading the Locked Version in package-lock.json
The direct fix updates the resolved nanoid version from 3.3.16 to 3.3.18:
Before:
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="
}
After:
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="
}
The new integrity hash (sha512-DTg4...) is cryptographically bound to the patched 3.3.18 tarball, ensuring npm cannot silently substitute a different version.
Part 2: Adding overrides to package.json
Simply updating package-lock.json is not sufficient on its own. Other packages in the dependency tree may themselves depend on nanoid and could resolve to the vulnerable version when npm install is run fresh. The overrides field in package.json solves this:
"overrides": {
"nanoid": "3.3.18"
}
Before (no overrides):
{
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.3.0"
}
}
After (with enforced override):
{
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.3.0"
},
"overrides": {
"nanoid": "3.3.18"
}
}
The overrides field (introduced in npm 8.3.0) instructs npm to resolve all occurrences of nanoid — whether direct or transitive — to exactly 3.3.18. This prevents a scenario where a nested dependency like postcss or vite pulls in nanoid 3.3.16 through its own dependency chain.
What Changed in nanoid 3.3.18?
The patch in nanoid 3.3.18 adds a bounded retry mechanism or adjusts the mask calculation to guarantee that the rejection-sampling loop always makes forward progress, regardless of the alphabet configuration. The fix ensures that the loop has a mathematically provable exit condition for all valid alphabet inputs.
Prevention & Best Practices
1. Use overrides (npm) or resolutions (Yarn) for Transitive Vulnerabilities
When a vulnerable package exists deep in your dependency tree, updating only the lock file may not be enough. Always pair lock file updates with an overrides entry:
// package.json (npm)
"overrides": {
"vulnerable-package": ">=patched-version"
}
// package.json (Yarn)
"resolutions": {
"vulnerable-package": ">=patched-version"
}
2. Audit Dependencies Regularly
Run automated scanners as part of your CI/CD pipeline:
# npm built-in audit
npm audit
# Trivy for container and filesystem scanning
trivy fs --scanners vuln .
# Snyk
snyk test
3. Never Pass Unvalidated User Input to ID Generation Functions
If your application allows user-configurable alphabets or ID lengths, validate them strictly before passing to nanoid:
const SAFE_ALPHABET_REGEX = /^[a-zA-Z0-9_-]{2,64}$/;
function safeCustomId(userAlphabet, size = 21) {
if (!SAFE_ALPHABET_REGEX.test(userAlphabet)) {
throw new Error('Invalid alphabet configuration');
}
return customAlphabet(userAlphabet, size)();
}
4. Pin Exact Versions for Security-Critical Dependencies
For libraries involved in authentication, session management, or ID generation, prefer exact version pinning over range specifiers:
// Prefer this for security-critical deps:
"nanoid": "3.3.18"
// Over this:
"nanoid": "^3.3.0"
5. Monitor CVE Feeds for Your Dependencies
Subscribe to:
- GitHub Security Advisories
- npm Security Advisories
- The nanoid GitHub repository's security tab
Security Standards Reference
- CWE-835: Loop with Unreachable Exit Condition
- OWASP A06:2021 – Vulnerable and Outdated Components
- OWASP DoS Cheat Sheet – guidance on preventing availability attacks
Key Takeaways
- The
customAlphabetfunction in nanoid < 3.3.18 is the specific vulnerable code path — not the defaultnanoid()function. If your code usescustomAlphabet, this vulnerability is directly relevant to you. - Updating
package-lock.jsonalone is insufficient — the"overrides"field inpackage.jsonis required to prevent transitive dependencies from re-introducing the vulnerable version during fresh installs. - A single hung request can take down an entire Node.js service — because the event loop is single-threaded, one infinite loop blocks all other request handling, making this DoS trivially effective.
- Trivy detected this at the
package-lock.jsonlevel — demonstrating that lock file scanning (not justpackage.jsonscanning) is essential for catching transitive dependency vulnerabilities. - The integrity hash change (
sha512-bzlK...→sha512-DTg4...) inpackage-lock.jsonis a cryptographic guarantee that the patched tarball is being used — always verify integrity hashes when reviewing security upgrades.
How Orbis AppSec Detected This
- Source: The
nanoidpackage resolved at version3.3.16inremotion-composer/package-lock.json, which is consumed by any code path invokingcustomAlphabet()with user-influenced or edge-case alphabet configurations. - Sink: The internal rejection-sampling
whileloop inside nanoid'scustomAlphabetimplementation — a loop with no upper iteration bound that can spin indefinitely given certain alphabet-to-mask ratios. - Missing control: No maximum iteration count or mathematical guarantee that the rejection-sampling loop terminates for all valid alphabet inputs in nanoid versions before 3.3.18.
- CWE: CWE-835 — Loop with Unreachable Exit Condition
- Fix: nanoid was upgraded from
3.3.16to3.3.18inpackage-lock.json, and an"overrides": { "nanoid": "3.3.18" }block was added topackage.jsonto enforce the patched version across all transitive dependencies.
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-67213 is a sharp reminder that even the most widely trusted, minimal utility libraries can harbor availability-destroying bugs. nanoid is used in hundreds of thousands of JavaScript projects precisely because it's small and fast — but that simplicity masked a loop termination flaw in its customAlphabet implementation that could freeze a Node.js process with a single crafted call.
The remediation for remotion-composer was precise and complete: upgrading the locked version to 3.3.18 and adding an overrides block to prevent the vulnerable version from creeping back through transitive dependencies. Neither change alone would have been sufficient.
For developers building on Node.js, this vulnerability underscores three durable lessons: scan your lock files (not just your manifests), enforce patched versions with overrides, and treat any library involved in ID generation as a security-sensitive component deserving careful version management.