How Unsafe Random Functions Happen in Node.js form-data and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Unsafe PRNG for multipart boundary generation |
| CWE | CWE-338 |
| Language | JavaScript / Node.js |
| Risk | Attackers can predict multipart boundaries, enabling content injection or request manipulation |
| Root Cause |form-datausedMath.random()instead of a cryptographically secure random source |
| Fix | Upgradeform-datato 4.0.6 and enforce viapackage.jsonoverrides |
Direct Answer
CVE-2025-7783 is a critical vulnerability (CWE-338) in the form-data npm package where a cryptographically weak pseudo-random number generator (PRNG) — specifically Math.random() — was used to generate multipart form boundaries, making them predictable to attackers. This affects Node.js applications that construct multipart/form-data requests. The fix is to upgrade form-data to version 4.0.6 and use a package.json overrides field to force all transitive dependencies to use the patched version, preventing older vulnerable versions from being pulled in through indirect dependencies like request.
Introduction
The package-lock.json file in this Node.js project pinned form-data at version 2.3.3 — a version that contains a subtle but critical flaw: it generates multipart form boundaries using Math.random(), JavaScript's built-in pseudo-random number generator, which is not cryptographically secure. This flaw is tracked as CVE-2025-7783 and carries a critical severity rating.
Multipart boundaries are the delimiter strings that separate fields and file attachments in multipart/form-data HTTP requests. They look like this in a raw HTTP request:
Content-Type: multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW
------FormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="upload.png"
...
If an attacker can predict the boundary string — which becomes trivially possible when Math.random() is the source of entropy — they can craft requests that manipulate how the server parses multipart payloads. For developers building APIs, file upload services, or any system that constructs outbound multipart requests programmatically, this vulnerability is a silent threat hiding in a widely-trusted package.
The Vulnerability Explained
What's Wrong with Math.random()?
Math.random() in JavaScript is designed for statistical randomness, not security. Its output is seeded by the JavaScript engine and is predictable given enough observed outputs or knowledge of the seed state. The V8 engine (used by Node.js) implements Math.random() using the xorshift128+ algorithm — fast, but entirely unsuitable for generating security-sensitive values.
In form-data versions prior to the patch, the boundary string for multipart requests was generated using a function roughly equivalent to:
// Vulnerable pattern in form-data < 2.5.4 / 3.0.4 / 4.0.4
function generateBoundary() {
var boundary = '--------------------------';
for (var i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(10);
}
return boundary;
}
The critical problem here: Math.random() produces a deterministic sequence once the seed is known. In server-side environments where an attacker can make many requests and observe timing or other side-channel information, the seed can be inferred — and future (or past) boundary values can be predicted.
How Could This Be Exploited?
Consider a Node.js service that:
1. Accepts a user-initiated action (e.g., uploading a profile picture)
2. Internally forwards that file to a backend API using form-data to construct a multipart/form-data request
An attacker who can:
- Observe multiple outbound boundary strings (e.g., through error messages, logs, or timing analysis)
- Or influence when the form-data boundary is generated relative to other Math.random() calls in the application
...could predict the next boundary string. With a known boundary, they could craft a malicious payload that:
- Injects additional form fields into the multipart body by embedding the boundary string inside a field value, causing the server to misparse the request
- Truncates or replaces file content by injecting a premature boundary, effectively splitting or corrupting the upload
- Bypasses server-side validation that relies on field ordering or field counts in the multipart body
This is a content injection attack against multipart parsing — a class of vulnerability that has historically been used to bypass file type checks, inject malicious filenames, or smuggle unauthorized data into backend systems.
Real-World Impact for This Application
In the affected project, form-data at version 2.3.3 was present both as a direct dependency resolution and as a transitive dependency pulled in by the request package (which pinned form-data: ~2.3.2). This means even if the top-level dependency was updated, the vulnerable version could still be instantiated through request's dependency subtree — a subtle but important attack surface that the fix specifically addresses.
The Fix
The remediation involved two coordinated changes: updating package-lock.json to resolve to the patched version, and adding a package.json overrides directive to enforce the patched version across the entire dependency tree, including transitive consumers like request.
Change 1: package-lock.json — Resolving to the Patched Version
The lock file was updated to point form-data to version 4.0.6 instead of the vulnerable 2.3.3. Additionally, the request package's pinned dependency on form-data: ~2.3.2 was overridden:
- "form-data": "~2.3.2",
+ "form-data": "4.0.6",
This ensures that when request is installed, it no longer pulls in the vulnerable 2.3.x range.
Change 2: package.json — Enforcing the Override Globally
The most important change for long-term security is the addition of the overrides field in package.json:
+ "overrides": {
+ "form-data": "4.0.6"
+ }
Before:
{
"dependencies": {
"perlin-noise": "^0.0.1",
"sharp": "^0.33.1"
}
}
After:
{
"dependencies": {
"perlin-noise": "^0.0.1",
"sharp": "^0.33.1"
},
"overrides": {
"form-data": "4.0.6"
}
}
The overrides field (introduced in npm 8.3.0) instructs npm to forcibly resolve all instances of form-data — regardless of which package requested it and what version range they specified — to 4.0.6. This is the critical defense against transitive dependency vulnerabilities, where a direct dependency (like request) drags in an outdated, vulnerable version of a sub-dependency.
Why This Two-File Fix Is Necessary
Without the overrides in package.json, running npm install in the future could silently re-introduce the vulnerable 2.3.3 version through request's ~2.3.2 version range. The lock file change alone is fragile — it can be overwritten. The overrides directive makes the security constraint durable and declarative, surviving future npm install runs.
In form-data 4.0.4+, the boundary generation was patched to use crypto.randomBytes() — Node.js's cryptographically secure random byte generator — instead of Math.random(). The patched implementation looks like:
// Secure pattern in form-data >= 4.0.4
const crypto = require('crypto');
function generateBoundary() {
return crypto.randomBytes(20).toString('hex');
}
crypto.randomBytes() draws entropy from the operating system's secure random source (e.g., /dev/urandom on Linux, CryptGenRandom on Windows), making the output computationally infeasible to predict.
Prevention & Best Practices
1. Never Use Math.random() for Security-Sensitive Values
Math.random() is appropriate for game mechanics, UI animations, and statistical sampling. It is never appropriate for:
- Boundary strings in multipart requests
- Session tokens or nonces
- CSRF tokens
- File upload identifiers
- Any value an attacker must not be able to predict
Always use crypto.randomBytes() or crypto.randomUUID() in Node.js for these purposes.
2. Use overrides (npm) or resolutions (Yarn) for Transitive Vulnerabilities
When a vulnerability exists in a transitive dependency, updating your direct dependency may not be enough. Use the appropriate mechanism for your package manager:
npm (v8.3.0+):
"overrides": {
"vulnerable-package": ">=safe-version"
}
Yarn (v1):
"resolutions": {
"vulnerable-package": ">=safe-version"
}
3. Run Dependency Audits Regularly
Integrate vulnerability scanning into your CI/CD pipeline:
# npm built-in audit
npm audit
# Trivy (detects CVE-2025-7783 and similar)
trivy fs --scanners vuln .
# Snyk
snyk test
4. Pin and Review package-lock.json
Always commit your package-lock.json to version control. Review it during code review for unexpected version changes. Automated tools like Dependabot and Renovate can keep it current.
5. Relevant Standards
- CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
- OWASP Cryptographic Failures (formerly A3:2017 Sensitive Data Exposure): Covers the use of weak algorithms and RNGs in security-sensitive contexts
- NIST SP 800-90A: Recommendation for Random Number Generation Using Deterministic Random Bit Generators
Key Takeaways
Math.random()inform-data< 2.5.4/3.0.4/4.0.4 generates predictable multipart boundaries — an attacker who can observe or infer boundary values can inject content into multipart requests- Updating your direct dependency isn't always enough — the
requestpackage'sform-data: ~2.3.2pin meant the vulnerable version could still be installed transitively without theoverridesfix - The
package.jsonoverridesfield is a durable security control — it survives futurenpm installruns and prevents accidental re-introduction of the vulnerable version crypto.randomBytes()is the correct replacement forMath.random()in boundary generation — it uses OS-level entropy and is computationally infeasible to predict- Trivy's static analysis caught this before it was confirmed reachable — scanning your
package-lock.jsonfor known CVEs is a low-cost, high-value security practice
How Orbis AppSec Detected This
- Source: The
form-datapackage version2.3.3resolved inpackage-lock.json, which is pulled in both directly and transitively through therequestpackage'sform-data: ~2.3.2dependency range - Sink: The boundary generation function inside
form-data's multipart stream constructor, which calledMath.random()to produce the delimiter string embedded inContent-Type: multipart/form-data; boundary=...headers - Missing control: No cryptographically secure entropy source was used;
Math.random()provided no unpredictability guarantees, and no version constraint prevented transitive consumers from loading the vulnerable version - CWE: CWE-338 — Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
- Fix: Upgraded
form-datato4.0.6(which usescrypto.randomBytes()for boundary generation) and added anoverridesdirective inpackage.jsonto enforce this 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-2025-7783 is a reminder that security vulnerabilities can hide in the most mundane-seeming code paths — boundary string generation isn't where most developers think to look for critical flaws. But the choice between Math.random() and crypto.randomBytes() is the difference between a predictable, exploitable value and one that's computationally secure.
What makes this vulnerability particularly tricky is the transitive dependency problem: even if you knew to update form-data, the request package's pinned ~2.3.2 range would silently pull the vulnerable version back in. The correct fix required both updating the resolved version in package-lock.json and adding an overrides directive in package.json to make the constraint durable.
For Node.js developers: audit your package-lock.json for known CVEs, use overrides aggressively when transitive vulnerabilities are involved, and treat Math.random() as off-limits for any value that must be unpredictable to an adversary.