Back to Blog
critical SEVERITY6 min read

How insecure nonce generation with Math.random() happens in Node.js HTTP Digest authentication and how to fix it

A critical vulnerability was discovered in `lib/cam.js` where the HTTP Digest authentication client nonce (cnonce) was generated using `Math.random().toString(36)` — a cryptographically insecure source of randomness. An attacker observing authentication exchanges could predict future cnonce values and forge valid authentication responses. The fix replaces this with `crypto.randomBytes(4).toString('hex')`, providing cryptographically secure random values.

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

Answer Summary

This is a cryptographically insecure PRNG vulnerability (CWE-330) in a Node.js ONVIF camera library where `Math.random()` was used to generate the client nonce (cnonce) for HTTP Digest authentication in `lib/cam.js`. The fix replaces `Math.random().toString(36)` with `crypto.randomBytes(4).toString('hex')` to produce unpredictable, cryptographically secure nonce values that cannot be brute-forced or predicted by an attacker.

Vulnerability at a Glance

cweCWE-330 (Use of Insufficiently Random Values)
fixReplace Math.random() with crypto.randomBytes(4).toString('hex')
riskAttacker can predict authentication nonces and forge valid Digest auth responses to IP cameras
languageJavaScript (Node.js)
root causeMath.random() used to generate HTTP Digest authentication cnonce value
vulnerabilityInsecure PRNG for cryptographic nonce generation

How Insecure Nonce Generation with Math.random() Happens in Node.js HTTP Digest Authentication and How to Fix It

Introduction

The lib/cam.js file in an ONVIF camera control library handles HTTP Digest authentication for communicating with IP cameras over the network. At line 418, the digestAuth method on Cam.prototype generated a client nonce (cnonce) using Math.random().toString(36) fed into an MD5 hash — a pattern that looks secure at first glance but is fundamentally broken.

This vulnerability is particularly dangerous because this is a Node.js library consumed by downstream packages. Every application using this library to authenticate with security cameras was generating predictable nonces, potentially allowing an attacker on the network to forge authentication responses and gain unauthorized access to camera feeds.

The Vulnerability Explained

HTTP Digest authentication is a challenge-response protocol designed to avoid sending passwords in cleartext. When a server challenges a client, the client must respond with a hash that incorporates several values, including a client nonce (cnonce). The cnonce serves a critical purpose: it prevents chosen-plaintext attacks and replay attacks by ensuring the client contributes unpredictable randomness to the authentication exchange.

Here's the vulnerable code from lib/cam.js at line 418:

if (typeof challenge.qop === 'string' && challenge.qop === 'auth') {
    const cnonceHash = crypto.createHash('md5');
    cnonceHash.update(Math.random().toString(36));
    cnonce = cnonceHash.digest('hex').substring(0, 8);
    nc = this.updateNC();
}

Why this is broken:

  1. Math.random() is not cryptographically secure. JavaScript's Math.random() uses algorithms like xorshift128+ that are designed for speed, not security. The internal state can be reconstructed from observed outputs.

  2. MD5 hashing doesn't add entropy. Wrapping Math.random() in crypto.createHash('md5') is security theater. A hash function is deterministic — if the input is predictable, the output is predictable. You cannot create entropy by hashing; you can only preserve it.

  3. The state space is limited. Math.random() produces a 64-bit floating point number, but the actual entropy of the V8 PRNG state is reconstructable from as few as 3-4 observed outputs.

Attack scenario specific to this code:

An attacker positioned on the same network as a camera (common in corporate environments) observes several Digest authentication exchanges between the ONVIF client and the camera. By collecting multiple cnonce values from the authentication headers, the attacker can:

  1. Reconstruct the internal state of Math.random() using known techniques (Z3 solver, lattice attacks on xorshift128+)
  2. Predict future cnonce values
  3. Pre-compute valid Digest authentication responses
  4. Forge authentication to the camera, gaining access to video feeds, PTZ controls, or device configuration

This is especially concerning because ONVIF cameras are security devices — compromising their authentication defeats the entire purpose of the physical security system.

The Fix

The fix replaces the insecure random generation with Node.js's cryptographically secure random byte generator:

Before (vulnerable):

if (typeof challenge.qop === 'string' && challenge.qop === 'auth') {
    const cnonceHash = crypto.createHash('md5');
    cnonceHash.update(Math.random().toString(36));
    cnonce = cnonceHash.digest('hex').substring(0, 8);
    nc = this.updateNC();
}

After (fixed):

if (typeof challenge.qop === 'string' && challenge.qop === 'auth') {
    cnonce = crypto.randomBytes(4).toString('hex');
    nc = this.updateNC();
}

Why this fix works:

  1. crypto.randomBytes(4) generates 4 bytes (32 bits) from the operating system's CSPRNG (/dev/urandom on Linux, CryptGenRandom on Windows). This output is cryptographically unpredictable.

  2. .toString('hex') converts the 4 random bytes to an 8-character hexadecimal string — exactly the same length as the original substring(0, 8) on the MD5 hex digest.

  3. Behavioral equivalence is preserved. The cnonce is still an 8-character hex string, so the Digest authentication protocol exchange works identically. The only difference is the quality of randomness.

  4. The code is simpler. Three lines become one. The unnecessary MD5 hash intermediate step is eliminated, making the code both more secure and more readable.

The crypto module was already imported in this file (it was being used for the MD5 hash), so no new dependencies were needed.

Prevention & Best Practices

Rule of thumb: If a value must be unpredictable to an attacker, never use Math.random().

Here's a decision framework for Node.js developers:

Use Case Safe API
Nonces, tokens, session IDs crypto.randomBytes()
UUIDs crypto.randomUUID()
Random integers in a range crypto.randomInt(min, max)
Non-security shuffling/sampling Math.random() (acceptable)

Detection tools:

  • ESLint: Use eslint-plugin-security which flags Math.random() in security contexts
  • Semgrep: Rules like javascript.lang.security.insecure-randomness.insecure-randomness catch this pattern
  • CodeQL: The js/insecure-randomness query identifies Math.random() flowing into security-sensitive operations

Security standards:

  • OWASP: The Cryptographic Failures category (formerly "Sensitive Data Exposure") covers insufficient randomness
  • CWE-330: Use of Insufficiently Random Values
  • CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
  • NIST SP 800-90A: Recommends approved DRBG mechanisms for security applications

Key Takeaways

  • Never use Math.random() for authentication nonces — even when wrapped in a hash function like MD5, the output remains predictable because hashing cannot create entropy
  • The crypto module is already available in Node.js — there's zero dependency cost to using crypto.randomBytes() instead of Math.random()
  • HTTP Digest auth cnonce values are security-critical — they prevent replay attacks, so predictable cnonces undermine the entire authentication scheme
  • Hashing insecure input is not a fix — the original code used crypto.createHash('md5') which gave a false sense of security while providing none
  • Library vulnerabilities cascade downstream — this camera control library is used by many applications, so one insecure nonce generation affected every consumer

How Orbis AppSec Detected This

  • Source: Math.random() call at lib/cam.js:419, producing a predictable pseudo-random value
  • Sink: The cnonce variable used in HTTP Digest authentication response construction in Cam.prototype.digestAuth
  • Missing control: No cryptographically secure random number generator (CSPRNG) was used for the security-critical nonce value
  • CWE: CWE-330 (Use of Insufficiently Random Values)
  • Fix: Replaced Math.random().toString(36) piped through MD5 with crypto.randomBytes(4).toString('hex') to generate a cryptographically unpredictable 8-character hex cnonce

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

This vulnerability demonstrates a common anti-pattern in JavaScript security: using Math.random() where cryptographic randomness is required, then attempting to "strengthen" it with a hash function. The hash adds complexity but not security. The fix is elegant in its simplicity — one line of crypto.randomBytes() replaces three lines of insecure code while maintaining identical protocol behavior.

For any developer working with authentication protocols, the lesson is clear: if a value's unpredictability is a security requirement, reach for your language's CSPRNG API from the start. In Node.js, that means crypto.randomBytes(), crypto.randomInt(), or crypto.randomUUID() — never Math.random().

References

Prevention and further reading

Frequently Asked Questions

Is hashing Math.random() output with MD5 enough to make it secure?

No. Hashing a predictable input with MD5 (or any hash) does not add entropy. If the input to the hash is predictable, the output is equally predictable. The fix must address the entropy source, not the post-processing.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #450

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

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