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

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #36

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.