How Unsafe Random Function Usage Happens in Node.js form-data and How to Fix It
In the ocapi-proxy project, a critical vulnerability was discovered in a transitive dependency: the form-data npm package. Trivy flagged CVE-2025-7783 in package-lock.json, revealing that both the direct node_modules/form-data (version 2.3.3) and a nested copy under node_modules/axios/node_modules/form-data (version 4.0.5) were affected by an unsafe random number generator used to construct multipart form boundaries.
This might sound like a low-level implementation detail, but the consequences are significant: predictable multipart boundaries can be exploited to smuggle data across field boundaries, potentially bypassing server-side validation logic or injecting unexpected content into form fields.
The Vulnerability Explained
What Are Multipart Form Boundaries?
When form-data constructs a multipart/form-data HTTP request body, it generates a unique boundary string that separates individual form fields. This boundary looks something like:
---------------------------123456789abcdef
Content-Disposition: form-data; name="file"; filename="upload.txt"
<file content here>
---------------------------123456789abcdef--
The boundary must be unique and unpredictable — it must not appear anywhere within the actual field content. If it does, a parser will prematurely terminate the field and treat the rest as a new part.
The Vulnerable Code Pattern
Before the fix, form-data used JavaScript's Math.random() to generate these boundary strings. Math.random() is a pseudo-random number generator (PRNG) — it is explicitly not designed for security-sensitive use. Its output is:
- Seeded deterministically in many V8 engine versions
- Statistically predictable if an attacker can observe enough outputs
- Not suitable for generating secrets, tokens, or structural delimiters in security contexts
The vulnerable package version in package-lock.json was pinned to:
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
...
}
And a second copy was nested under axios:
"node_modules/axios/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
...
}
Both versions relied on Math.random() for boundary generation — meaning two separate vulnerable copies existed in the dependency tree simultaneously.
How an Attacker Could Exploit This
Consider a scenario where ocapi-proxy forwards multipart form uploads to an upstream API. An attacker who:
- Observes multiple requests from the application (or triggers them via a public endpoint), and
- Collects enough
Math.random()outputs embedded in boundary strings
...could reconstruct the PRNG state and predict future boundary values. With a predicted boundary in hand, the attacker crafts a file upload where the file content itself contains the predicted boundary string:
---------------------------<predicted-boundary>
Content-Disposition: form-data; name="description"
safe text
---------------------------<predicted-boundary>
Content-Disposition: form-data; name="file"; filename="evil.txt"
<injected content that the upstream API treats as a new field>
---------------------------<predicted-boundary>--
The upstream parser sees this as a legitimately structured multipart body with an extra field — one the application never intended to send. Depending on the upstream API's behavior, this could:
- Override server-side parameters with attacker-controlled values
- Bypass field-level validation (e.g., inject a
role=adminfield) - Cause unexpected application logic to execute
This is classified under CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG).
The Fix
What Changed in the Diff
The pull request made two key structural changes to package-lock.json:
1. Removed the nested axios copy of form-data (4.0.5)
The entire node_modules/axios/node_modules/form-data block was deleted:
- "node_modules/axios/node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- }
- },
This eliminates the second vulnerable copy. Axios will now resolve to the single top-level form-data entry.
2. Upgraded the top-level form-data from 2.3.3 to 4.0.6
"node_modules/form-data": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
- "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"dependencies": {
"asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12"
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12",
+ "es-set-tostringtag": "^2.1.0"
}
}
Version 4.0.6 (patching the 4.0.4 security fix) replaces Math.random() with Node.js's crypto.randomBytes() for boundary generation, producing cryptographically unpredictable boundaries that cannot be reconstructed by an observer.
Why Both Changes Were Necessary
Simply upgrading the top-level entry to 4.0.6 would not have been sufficient — npm's hoisting behavior meant that axios was pinned to its own nested copy at 4.0.5. By removing that nested entry, the fix ensures a single, patched version is used across the entire dependency tree. This is a common but often overlooked attack surface: a package can appear "fixed" at the top level while a vulnerable nested copy remains active.
Prevention & Best Practices
1. Never Use Math.random() for Security-Sensitive Values
In Node.js, Math.random() is explicitly documented as not cryptographically secure. For any value that needs to be unguessable — boundaries, tokens, nonces, session IDs — use:
const crypto = require('crypto');
const boundary = crypto.randomBytes(16).toString('hex');
2. Audit Your Full Dependency Tree, Not Just Direct Dependencies
CVE-2025-7783 existed in two places in this project's lock file — one direct and one transitive. Use tools that inspect the full tree:
npm audit
trivy fs --scanners vuln .
3. Lock File Hygiene: Watch for Nested Duplicates
When a nested copy of a package exists (e.g., node_modules/axios/node_modules/form-data), it can silently shadow a patched top-level version. After any security upgrade, verify no nested copies remain:
find node_modules -name "package.json" -path "*/form-data/package.json" | xargs grep '"version"'
4. Pin to Patch Releases in package.json
Using ^4.0.4 in package.json allows npm to resolve to patched versions automatically. Avoid overly loose ranges like * or >=2 for security-sensitive packages.
5. Enable Automated Dependency Scanning in CI
Integrate Trivy, Snyk, or npm audit into your CI pipeline so vulnerable dependency versions are caught before they reach production:
- name: Security audit
run: trivy fs --exit-code 1 --severity CRITICAL,HIGH .
Security Standards Reference
- CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
- OWASP A02:2021 – Cryptographic Failures: covers the use of weak or inappropriate cryptographic algorithms
- OWASP Dependency-Check: recommends automated scanning of all transitive dependencies
Key Takeaways
Math.random()in form-data 2.3.3 and 4.0.5 generated predictable multipart boundaries — a structural delimiter that must be unguessable to prevent boundary injection attacks.- Two vulnerable copies existed simultaneously in
package-lock.json: one atnode_modules/form-data(2.3.3) and one nested undernode_modules/axios/node_modules/form-data(4.0.5) — both had to be addressed. - Removing the nested axios copy was as important as upgrading the top-level entry; leaving it would have kept the vulnerability active for all axios-initiated requests.
- The fix consolidates to a single form-data 4.0.6 entry, which uses
crypto.randomBytes()internally — making boundaries cryptographically random and unpredictable. - Trivy's static analysis caught this in
package-lock.jsonbefore it could be exploited, demonstrating the value of scanning lock files (not justpackage.json) in your CI pipeline.
How Orbis AppSec Detected This
- Source: The
package-lock.jsonfile resolvedform-datato version 2.3.3 (direct) and 4.0.5 (nested under axios), both of which useMath.random()to generate multipart form boundaries — a value derived from a seeded, non-cryptographic PRNG. - Sink: The boundary string generated by
Math.random()is embedded directly into theContent-Type: multipart/form-data; boundary=<value>header and into the serialized request body. Any code path inocapi-proxythat constructs a multipart request (directly or via axios) passes through this vulnerable code. - Missing control: No cryptographically secure random source (
crypto.randomBytes()) was used. The boundary value was not validated or re-generated with a CSPRNG before being written into the HTTP body. - CWE: CWE-338 — Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
- Fix: Both the top-level
form-dataentry and the nestedaxios-scoped copy were upgraded to 4.0.6, 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 security vulnerabilities don't always look like classic injection flaws or authentication bypasses. Sometimes, a single function choice — Math.random() instead of crypto.randomBytes() — buried deep in a transitive dependency can open the door to boundary injection and payload smuggling attacks.
What made this case particularly tricky was the dual presence of the vulnerable package: both a direct dependency at version 2.3.3 and a nested copy under axios at 4.0.5 needed to be addressed. A partial fix — upgrading only one — would have left the vulnerability active.
The broader lesson: treat your package-lock.json as a first-class security artifact. Scan it, audit it, and verify that security upgrades propagate through the entire resolved dependency tree — not just the top-level entries.