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

medium

How OAuth token audience bypass happens in Node.js serverless functions and how to fix it

A critical OAuth authentication vulnerability in a Netlify serverless function allowed any valid Google OAuth token—even those issued to completely different applications—to authenticate successfully. The fix adds proper audience (aud) claim verification using Google's tokeninfo endpoint to ensure only tokens issued specifically for this application are accepted.

high

How signature verification bypass happens in Node.js crypto and how to fix it

A high-severity signature verification bypass was discovered in `apps/panel/panel.js` where the `JMkey` variable was passed directly to `Buffer.from(JMkey, 'hex')` without validating its format. An attacker could supply a malformed hex string to cause silent failures or unexpected behavior in the RSA-SHA256 verification process, potentially bypassing signature checks entirely. The fix adds strict hex format validation before processing.

critical

How missing authorization enforcement happens in Node.js Express routers and how to fix it

A critical authorization bypass was discovered in lib/router.js where readOnly and noDelete configuration options were only enforced through UI controls, not server-side middleware. Any authenticated user could bypass these restrictions by sending direct HTTP requests to perform destructive operations like database deletion or document modification. The fix adds Express middleware that enforces these security modes at the server level, blocking POST, PUT, and DELETE requests when appropriate.

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

critical

How XSS via unescaped sender_name happens in JavaScript chat widgets and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `frontend/scripts/chat-widget.js` allowed attackers to inject arbitrary JavaScript by crafting a malicious `sender_name` field, which was interpolated directly into a DOM template string without HTML encoding. The `renderFileContent()` function compounded the risk by also inserting unsanitized `file.name` and `file.url` values into `img`, `span`, and `anchor` elements. The fix applies `AppUtils.escapeHTML()` to every user-controlled value befo