How Unsafe Random Function in form-data Happens in Node.js and How to Fix It
Introduction
In the agario-bots-proxies project, we discovered a critical vulnerability stemming from an outdated version of the form-data npm package (version 2.3.3) locked in ExampleScripts/agario-bots2/agario-bots-proxies/pnpm-lock.yaml. The package used Math.random() — JavaScript's built-in but cryptographically insecure pseudo-random number generator — to create multipart form boundary strings.
This matters because form-data is one of the most widely depended-upon packages in the Node.js ecosystem. It's pulled in transitively by request, request-promise, and countless HTTP client libraries. In this project, the dependency chain flows through request@2.88.0 → form-data@2.3.3, meaning every HTTP request that included multipart form data was using predictable boundaries.
The Vulnerability Explained
When you send a multipart form request (e.g., file uploads), the HTTP body uses a boundary string to separate different form fields. This boundary must be unique and unpredictable — if an attacker can guess it, they can craft payloads that break out of one form field and inject content into another.
In form-data versions prior to 2.5.4/3.0.4/4.0.4, the boundary was generated using code like:
FormData.prototype._generateBoundary = function() {
// This uses Math.random() which is NOT cryptographically secure
var boundary = '--------------------------';
for (var i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16);
}
this._boundary = boundary;
};
Math.random() in V8 (Node.js's JavaScript engine) uses the xorshift128+ algorithm. This PRNG has well-documented properties:
- It is seeded with limited entropy — the internal state can be reconstructed from a small number of observed outputs.
- It is deterministic — once the state is known, all future outputs are predictable.
- State recovery is practical — researchers have demonstrated recovering xorshift128+ state from as few as 3-4 observed
Math.random()outputs.
Attack Scenario
Consider this project's architecture: it's a bot proxy system for agar.io that makes HTTP requests through proxies. Here's how an attacker could exploit predictable boundaries:
- Observe: An attacker monitoring network traffic (or operating a malicious proxy in the
socks-proxy-agentchain) captures a few multipart requests and extracts their boundary strings. - Predict: Using the observed boundaries, the attacker recovers the
Math.random()PRNG state and predicts future boundary values. - Inject: The attacker crafts a response or man-in-the-middle payload containing the predicted boundary, allowing them to inject additional form fields or manipulate the multipart structure.
- Exploit: Injected content could include malicious file data, modified parameters, or crafted payloads that the receiving server processes as legitimate form fields.
In this specific project, where requests flow through multiple proxy layers (https-proxy-agent, socks-proxy-agent), the attack surface is amplified — any compromised proxy in the chain could observe and exploit the predictable boundaries.
The Fix
The fix upgrades form-data from the vulnerable version 2.3.3 to version 4.0.4, which uses crypto.randomBytes() for boundary generation.
Before (package.json):
{
"dependencies": {
"colour": "^0.7.1",
"express": "^4.17.1",
"https-proxy-agent": "^2.2.2",
"murmurhash-js": "^1.0.0",
"request": "^2.88.0",
"request-promise": "^4.2.4",
"ws": "^7.1.2",
"socks-proxy-agent": "^4.0.2"
}
}
After (package.json):
{
"dependencies": {
"colour": "^0.7.1",
"express": "^4.17.1",
"form-data": "4.0.4",
"https-proxy-agent": "^2.2.2",
"murmurhash-js": "^1.0.0",
"request": "^2.88.0",
"request-promise": "^4.2.4",
"socks-proxy-agent": "^4.0.2",
"ws": "^7.1.2"
}
}
Two critical changes were made:
-
package.json— Added"form-data": "4.0.4"as a direct, pinned dependency. This ensures the fixed version is resolved regardless of what transitive dependencies (likerequest) might pull in. By pinning to4.0.4exactly (no caret^), the project guarantees this specific patched version. -
pnpm-lock.yaml— The lockfile was regenerated (upgraded from lockfile version 5.1 to 9.0) to resolve the new dependency graph withform-data@4.0.4properly integrated. The lock ensures reproducible installs with the secure version.
The fixed version of form-data replaces the boundary generation with:
// Secure boundary generation in form-data >= 4.0.4
FormData.prototype._generateBoundary = function() {
this._boundary = crypto.randomBytes(24).toString('hex');
};
crypto.randomBytes() draws from the operating system's CSPRNG (e.g., /dev/urandom on Linux, CryptGenRandom on Windows), providing entropy that is computationally infeasible to predict.
Prevention & Best Practices
-
Never use
Math.random()for security-sensitive values: Boundaries, tokens, nonces, session IDs, CSRF tokens — all must usecrypto.randomBytes()orcrypto.randomUUID(). -
Pin critical security dependencies directly: Even if a package is only a transitive dependency, adding it as a direct dependency with a pinned version (like
"form-data": "4.0.4") ensures your project uses the patched version. -
Regularly scan lockfiles: Tools like Trivy, Snyk, and npm audit can detect known vulnerabilities in your resolved dependency tree — including transitive dependencies buried in lockfiles.
-
Keep lockfiles updated: The original
pnpm-lock.yamlused lockfile version 5.1 with outdated resolution. Modern lockfile formats (9.0) provide better dependency resolution and security metadata. -
Audit proxy chains: When your application routes traffic through proxies (as this bot system does with
https-proxy-agentandsocks-proxy-agent), the security of data in transit becomes even more critical. Predictable boundaries in proxy environments dramatically increase exploitation risk.
Key Takeaways
form-data@2.3.3's use ofMath.random()for boundary generation made all multipart requests from this bot proxy system predictable — an attacker observing just a few requests through the proxy chain could predict all future boundaries.- Transitive dependencies are attack surface: The vulnerable
form-datawas pulled in throughrequest@2.88.0→form-data@2.3.3, not directly declared — making it invisible without lockfile scanning. - Pinning
form-dataas a direct dependency at version 4.0.4 overrides the vulnerable transitive version, a pattern useful when you can't easily upgrade the parent dependency. - The pnpm lockfile upgrade from v5.1 to v9.0 wasn't just cosmetic — modern lockfile formats provide better integrity guarantees and dependency resolution.
- In proxy-heavy architectures like this agar.io bot system, cryptographic weakness in HTTP primitives is amplified because every intermediary is a potential observer.
How Orbis AppSec Detected This
- Source: The
form-datapackage's internal boundary generation function, called whenever multipart form requests are constructed viarequestorrequest-promisein the bot proxy system. - Sink:
Math.random()calls withinform-data@2.3.3's_generateBoundary()method, producing predictable boundary strings used in HTTP multipart request bodies. - Missing control: No cryptographically secure random number generator was used for boundary generation;
Math.random()provided insufficient entropy. - CWE: CWE-330 (Use of Insufficiently Random Values)
- Fix: Upgraded
form-datafrom 2.3.3 to 4.0.4 by adding it as a direct pinned dependency inpackage.jsonand regenerating thepnpm-lock.yaml.
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 stark reminder that cryptographic security failures can lurk in the most fundamental layers of your dependency tree. A single Math.random() call in a boundary generation function — buried three levels deep in your node_modules — can undermine the integrity of every multipart HTTP request your application sends.
For this agar.io bot proxy system, where traffic flows through multiple proxy layers and the request library constructs multipart forms automatically, the impact was especially severe. The fix was straightforward: pin form-data@4.0.4 directly and let the secure crypto.randomBytes() implementation do its job.
Audit your lockfiles. Pin your security-critical dependencies. And never trust Math.random() with anything that needs to be unpredictable.