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

Frequently Asked Questions

What is insecure PRNG in authentication?

Using a non-cryptographic random number generator like Math.random() to produce security-critical values such as nonces, tokens, or session IDs. These generators have predictable output that attackers can reproduce or brute-force.

How do you prevent insecure nonce generation in Node.js?

Use the built-in `crypto` module's `crypto.randomBytes()` or `crypto.randomUUID()` functions, which draw from the operating system's cryptographically secure random number generator (CSPRNG).

What CWE is insecure PRNG?

CWE-330 (Use of Insufficiently Random Values) covers cases where insufficient randomness is used in security contexts, and CWE-338 (Use of Cryptographically Weak PRNG) is the more specific child entry.

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.

Can static analysis detect insecure PRNG usage?

Yes. Tools like Semgrep, ESLint security plugins, and CodeQL have rules that flag Math.random() usage in security-sensitive contexts such as authentication, token generation, and cryptographic operations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #450

Related Articles

critical

How Plaintext Credential Storage happens in JSON Configuration Files and how to fix it

A critical security issue was discovered in `assets/settings/global.json` where a real phone number (PII) was stored in plaintext alongside placeholder patterns for API keys and payment credentials. This design encouraged developers to substitute real credentials directly into a version-controlled file, creating a high risk of credential exposure via repository access or filesystem reads. The fix replaces the hardcoded phone number with a placeholder and reinforces safe configuration patterns.

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.