How Unsafe Random Functions Happen in Node.js Form Data and How to Fix It
Introduction
The example-apps/collector/package-lock.json file locks the dependencies for an Instana collector example application — a component that instruments Node.js services and sends telemetry data. Buried inside its dependency tree was a quietly dangerous flaw: the form-data package, pinned at version 2.3.3, was generating multipart form boundaries using Math.random(), a function that was never designed to produce unpredictable values in a security context.
This is CVE-2025-7783, rated critical, and it affects every application that uses a vulnerable version of form-data to send HTTP multipart requests — which includes a very large portion of the Node.js ecosystem, since form-data is a transitive dependency of popular packages like axios, node-fetch, and many others.
The Vulnerability Explained
What Is a Multipart Boundary?
When a browser or HTTP client sends a multipart/form-data request (used for file uploads and complex form submissions), it separates each field using a boundary string — a unique delimiter that must not appear inside any of the field values. A typical multipart request looks like this:
Content-Type: multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW
------FormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="upload.txt"
<file contents here>
------FormBoundary7MA4YWxkTrZu0gW--
The boundary is announced in the Content-Type header and used by the server to parse the body. If an attacker can predict or control the boundary string, they can inject their own boundary into field values and manipulate how the server parses the request.
The Root Cause: Math.random() for a Security-Sensitive Value
In the vulnerable versions of form-data (including 2.3.3 locked in this repository), the boundary was generated using JavaScript's Math.random(). Here is the conceptual pattern that was in use:
// VULNERABLE — from form-data before the fix
function generateBoundary() {
var boundary = '--------------------------';
for (var i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16);
}
return boundary;
}
Math.random() is a pseudo-random number generator (PRNG). It is seeded from a predictable internal state and is explicitly documented by every JavaScript engine as not suitable for cryptographic or security-sensitive use. An attacker with knowledge of the timing or environment of a Node.js process can potentially predict or brute-force the sequence of values produced by Math.random().
This maps directly to CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).
How This Can Be Exploited
Consider the Instana collector application in this repository. It uses form-data to send multipart HTTP requests carrying telemetry, traces, or profiling data. An attack scenario looks like this:
- Attacker observes timing: The attacker knows (or can estimate) when the Node.js process started, which seeds
Math.random(). - Attacker predicts the boundary: By modeling the PRNG state, the attacker predicts the boundary string that will be used for an upcoming multipart request.
- Attacker injects boundary into a field value: If any part of the uploaded data is attacker-influenced (e.g., a user-supplied filename, a log entry, or a trace attribute), the attacker embeds the predicted boundary string inside that value.
- Server misparses the request: The server's multipart parser sees the injected boundary as a legitimate field separator, splitting or corrupting the parsed data — potentially injecting a new field, truncating a file, or bypassing content validation.
In the context of an observability agent like Instana, this could mean corrupting trace data, injecting false telemetry, or bypassing security controls that rely on inspecting uploaded content.
The Fix
What Changed
The fix upgrades form-data from 2.3.3 to the patched versions 2.5.4, 3.0.4, and 4.0.4. The patched versions replace the Math.random()-based boundary generator with Node.js's built-in crypto.randomBytes(), which produces cryptographically secure random values.
The secure pattern used in the fixed versions looks like this:
// FIXED — form-data 2.5.4 / 3.0.4 / 4.0.4
var crypto = require('crypto');
function generateBoundary() {
return crypto.randomBytes(16).toString('hex');
}
crypto.randomBytes() draws entropy from the operating system's CSPRNG (e.g., /dev/urandom on Linux), making the output statistically indistinguishable from random and computationally infeasible to predict.
Before and After: package-lock.json
The package-lock.json change in example-apps/collector/ reflects the version bump. Here is the relevant portion of the diff:
- "@instana/collector": {
- "version": "1.119.1",
- ...
- "dependencies": {
- "@instana/core": "1.119.1",
- ...
- }
- }
+ "@instana/collector": {
+ "version": "6.5.0",
+ ...
+ "license": "MIT",
+ "dependencies": {
+ "@instana/core": "6.5.0",
+ ...
+ }
+ }
The @instana/collector package was also upgraded from 1.119.1 to 6.5.0 as part of this change, which brings in the patched form-data transitively. The @instana/autoprofile package similarly jumped from 1.119.1 to 6.5.0, updating its engine requirement from node >=6.4.0 to node >=18.19.0, reflecting the broader modernization of the dependency tree alongside the security fix.
Why Both package.json and package-lock.json Were Updated
package.json: Declares the direct dependency version constraint. Updating this ensures that futurenpm installruns will resolve to the patched version rather than silently re-installing the vulnerable one.package-lock.json: Locks the exact resolved version and its full sub-dependency tree. Without updating the lockfile, anpm cicommand (used in CI/CD pipelines) would continue installing the vulnerable2.3.3version regardless of whatpackage.jsonsays.
Both files must be updated together for the fix to be durable.
Prevention & Best Practices
1. Never Use Math.random() for Security-Sensitive Values
Math.random() is appropriate for games, UI animations, and non-security shuffles. It is never appropriate for:
- Tokens, session IDs, or nonces
- Cryptographic keys or salts
- Boundary strings in security-sensitive multipart requests
- Any value whose unpredictability is a security requirement
Use instead: crypto.randomBytes(n) (Node.js), crypto.getRandomValues() (browser), or a well-audited library like uuid v4 (which uses CSPRNG internally).
2. Audit Your Transitive Dependencies
form-data is rarely a direct dependency — it is pulled in by axios, superagent, node-fetch, and many other popular packages. Use npm ls form-data to find all versions resolved in your dependency tree:
npm ls form-data
If you see 2.3.3 or any version below 2.5.4 / 3.0.4 / 4.0.4, you are vulnerable.
3. Use Automated Dependency Scanning
Tools that can detect this class of vulnerability:
- Trivy (flagged this exact issue in the PR)
- npm audit / yarn audit
- Dependabot / Renovate for automated PRs
- Semgrep with the
javascript.lang.security.audit.math-randomrule
4. Keep Lockfiles in Version Control
Always commit package-lock.json or yarn.lock. Without a lockfile, npm install may resolve a different (potentially vulnerable) version than what was tested.
5. Reference Security Standards
- CWE-338: Use of Cryptographically Weak PRNG
- OWASP Cryptographic Failures (formerly Sensitive Data Exposure): OWASP Top 10 A02
- OWASP Cryptographic Storage Cheat Sheet: recommends CSPRNG for all security-sensitive random generation
Key Takeaways
Math.random()inform-dataproduced predictable multipart boundaries — a value that must be unpredictable to prevent boundary injection attacks.- The fix is a version upgrade to
form-data2.5.4, 3.0.4, or 4.0.4, which internally switches boundary generation tocrypto.randomBytes(). - Transitive dependencies are a real attack surface:
form-datawas not a direct dependency of this application — it came in through@instana/collector. Scanning the full dependency tree (not just direct dependencies) is essential. - Both
package.jsonandpackage-lock.jsonmust be updated to make a dependency fix durable across all install modes, includingnpm ciin CI/CD pipelines. - Trivy's static analysis of the lockfile was sufficient to detect this — you do not need runtime instrumentation to catch known-vulnerable dependency versions.
How Orbis AppSec Detected This
- Source: The
form-datapackage, resolved as a transitive dependency inexample-apps/collector/package-lock.json, generates a multipart boundary string that is included in every outbound HTTP multipart request. - Sink: The
generateBoundary()function insideform-data(versions < 2.5.4 / 3.0.4 / 4.0.4) callsMath.random()to produce a security-sensitive delimiter value. - Missing control: No cryptographically secure entropy source was used;
Math.random()provides no security guarantees and its output is predictable given knowledge of the PRNG state. - CWE: CWE-338 — Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).
- Fix:
form-datawas upgraded to2.5.4/3.0.4/4.0.4, which replacesMath.random()withcrypto.randomBytes()for boundary generation.
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 cryptographic correctness is not just about encryption algorithms — it extends to every value in your application whose unpredictability has security implications. A multipart form boundary sounds innocuous, but when it is generated with Math.random(), it becomes a predictable string that an attacker can weaponize to manipulate request parsing.
The fix is straightforward: upgrade form-data to 2.5.4, 3.0.4, or 4.0.4. But the broader lesson is to treat any randomly generated value in a security context with the same rigor you would apply to a password or a token. Use crypto.randomBytes(), audit your transitive dependencies regularly, and keep your lockfiles up to date.