Back to Blog
critical SEVERITY6 min read

How unsafe random function in form-data happens in Node.js and how to fix it

The `form-data` npm package used `Math.random()` — a cryptographically unsafe pseudo-random number generator — to generate multipart form boundaries. This critical vulnerability (CVE-2025-7783) allowed attackers to predict boundary strings and potentially inject malicious content into multipart requests. The fix upgrades `form-data` from version 2.3.3 to 4.0.4, which uses a cryptographically secure random source.

O
By Orbis AppSec
Published July 26, 2026Reviewed July 26, 2026

Answer Summary

CVE-2025-7783 is a critical vulnerability in the Node.js `form-data` package (CWE-330) where `Math.random()` was used to generate multipart form boundary strings, making them predictable. The fix is to upgrade `form-data` to version 2.5.4, 3.0.4, or 4.0.4, which replace the unsafe random function with a cryptographically secure alternative like `crypto.randomBytes()`.

Vulnerability at a Glance

cweCWE-330 (Use of Insufficiently Random Values)
fixUpgrade form-data to 4.0.4 which uses cryptographically secure randomness
riskPredictable multipart boundaries enabling request smuggling or content injection
languageJavaScript (Node.js)
root causeform-data used Math.random() instead of crypto.randomBytes() for boundary generation
vulnerabilityUnsafe random function in multipart boundary generation

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.0form-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:

  1. It is seeded with limited entropy — the internal state can be reconstructed from a small number of observed outputs.
  2. It is deterministic — once the state is known, all future outputs are predictable.
  3. 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:

  1. Observe: An attacker monitoring network traffic (or operating a malicious proxy in the socks-proxy-agent chain) captures a few multipart requests and extracts their boundary strings.
  2. Predict: Using the observed boundaries, the attacker recovers the Math.random() PRNG state and predicts future boundary values.
  3. 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.
  4. 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:

  1. 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 (like request) might pull in. By pinning to 4.0.4 exactly (no caret ^), the project guarantees this specific patched version.

  2. pnpm-lock.yaml — The lockfile was regenerated (upgraded from lockfile version 5.1 to 9.0) to resolve the new dependency graph with form-data@4.0.4 properly 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

  1. Never use Math.random() for security-sensitive values: Boundaries, tokens, nonces, session IDs, CSRF tokens — all must use crypto.randomBytes() or crypto.randomUUID().

  2. 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.

  3. 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.

  4. Keep lockfiles updated: The original pnpm-lock.yaml used lockfile version 5.1 with outdated resolution. Modern lockfile formats (9.0) provide better dependency resolution and security metadata.

  5. Audit proxy chains: When your application routes traffic through proxies (as this bot system does with https-proxy-agent and socks-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 of Math.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-data was pulled in through request@2.88.0form-data@2.3.3, not directly declared — making it invisible without lockfile scanning.
  • Pinning form-data as 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-data package's internal boundary generation function, called whenever multipart form requests are constructed via request or request-promise in the bot proxy system.
  • Sink: Math.random() calls within form-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-data from 2.3.3 to 4.0.4 by adding it as a direct pinned dependency in package.json and regenerating the pnpm-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.

References

Frequently Asked Questions

What is an unsafe random function vulnerability?

It occurs when security-sensitive operations like generating tokens, boundaries, or identifiers use predictable random number generators (like Math.random()) instead of cryptographically secure ones (like crypto.randomBytes()), allowing attackers to predict generated values.

How do you prevent unsafe random function vulnerabilities in Node.js?

Always use the `crypto` module's `randomBytes()` or `randomUUID()` for any security-relevant random value generation. Never use `Math.random()` for boundaries, tokens, session IDs, or any value that must be unpredictable.

What CWE is unsafe random function?

CWE-330 (Use of Insufficiently Random Values). Related entries include CWE-338 (Use of Cryptographically Weak Pseudo-Random Number Generator) and CWE-331 (Insufficient Entropy).

Is upgrading the dependency enough to prevent this vulnerability?

Yes, upgrading form-data to versions 2.5.4, 3.0.4, or 4.0.4 fully resolves CVE-2025-7783. However, you should audit your own codebase for similar Math.random() usage in security contexts.

Can static analysis detect unsafe random function usage?

Yes, tools like Trivy (which detected this CVE), Semgrep, and ESLint security plugins can flag Math.random() usage in security-sensitive contexts and identify vulnerable dependency versions.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #36

Related Articles

medium

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

high

How Arbitrary Code Execution via Template Imports Happens in JavaScript (lodash) and How to Fix It

A high-severity arbitrary code execution vulnerability (CVE-2026-4800) was discovered in lodash's template function, specifically in how it handles the `imports` option with untrusted input. The fix upgrades lodash from version 4.17.21 to 4.18.0 in the project's `package.json` and `yarn.lock`, eliminating the attack surface where crafted template imports could execute arbitrary code on the server.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js IP address parsing and how to fix it

A critical SSRF vulnerability (CVE-2026-69192) was discovered in the ip-address npm package version 10.2.0, which could allow attackers to bypass IP address validation and access internal services. The fix upgrades the dependency to version 10.3.1, which properly handles edge cases in IP address parsing that previously allowed trust-boundary bypasses.

critical

How Sensitive Data Exposure happens in Python web applications and how to fix it

A critical sensitive data exposure vulnerability was discovered in `nodes/google_gemini.py` where the Google Gemini API key was returned in plaintext through a web endpoint. The fix masks the token in API responses, preventing credential theft from any client that queries the token endpoint. This protects downstream users of this Node.js library from unauthorized access to their Google Gemini services.

high

How Authentication Bypass happens in Next.js App Router with Turbopack and how to fix it

A critical authentication bypass vulnerability (CVE-2026-64642) was discovered in Next.js versions prior to 16.2.11, specifically affecting App Router applications using Turbopack with a single locale configuration. This vulnerability allowed attackers to bypass middleware and proxy protections, potentially gaining unauthorized access to protected routes and resources that should have been secured by authentication checks.

critical

How SQL Injection Happens in CSV-to-SQL Converters and How to Fix It

A critical SQL injection vulnerability was discovered in the `csv2sql()` function in `src/data/converter/csv.js`, where CSV data and table names were directly interpolated into SQL INSERT statements without sanitization. The fix implements input validation through identifier sanitization and proper value escaping, eliminating the attack surface while preserving legitimate functionality.