CWE-331: Insufficient Entropy - JavaScript/Node.js
Overview
Insufficient entropy in JavaScript occurs when using Math.random() instead of cryptographically secure random sources like crypto.randomBytes() (Node.js) or crypto.getRandomValues() (Browser) for security-sensitive operations. Math.random() is designed for games and simulations, not cryptography, and produces predictable values.
Primary Defence: Use crypto.randomBytes() (Node.js) or crypto.getRandomValues() (browser) for all security-sensitive random value generation.
Common Vulnerable Patterns
Using Math.random() for token generation
// VULNERABLE - Predictable token generation
function generateToken() {
return Math.random().toString(36).substring(2);
}
Why this is vulnerable:
Math.random()uses a fast, deterministic PRNG (xorshift128+ in V8) that is not cryptographically secure.- The internal state can be recovered from a small number of outputs, making past and future values predictable.
- Base-36 encoding only changes representation, not security.
Using Date.now() with Math.random() for session IDs
// VULNERABLE - Session ID generation
function generateSessionId() {
return Date.now() + '-' + Math.random().toString(36);
}
Why this is vulnerable:
- The timestamp is observable, so it does not add secrecy.
Math.random()is typically seeded from time, giving low entropy that is easy to brute-force.- Combining two predictable sources still yields a predictable session ID.
Using Math.random() for API keys
// VULNERABLE - API key generation
function generateApiKey() {
let key = '';
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for (let i = 0; i < 32; i++) {
key += chars[Math.floor(Math.random() * chars.length)];
}
return key;
}
Why this is vulnerable:
- API keys are long-lived credentials and must be unguessable.
Math.random()is deterministic, so the same seed produces the same sequence.- If an attacker recovers the PRNG state, they can predict or forge keys.
Using Math.random() for encryption keys
// VULNERABLE - Encryption key from random
function generateKey() {
const key = new Uint8Array(32);
for (let i = 0; i < key.length; i++) {
key[i] = Math.floor(Math.random() * 256);
}
return key;
}
Why this is vulnerable:
- Encryption keys must be unpredictable;
Math.random()is not. - Predictable output lets attackers reproduce keys if they can infer the PRNG state.
- Once the state is known, all generated keys become guessable.
Using Math.random() for CSRF tokens
// VULNERABLE - CSRF token
function generateCsrfToken() {
return Math.random().toString(36) + Math.random().toString(36);
}
Why this is vulnerable:
- CSRF tokens must be unguessable to prevent forged requests.
Math.random()outputs are predictable to attackers who learn the PRNG state.- Multiple calls still draw from the same predictable sequence.
Using Math.random() for password reset tokens
// VULNERABLE - Password reset token
function generateResetToken() {
return Math.random().toString(16).slice(2, 10);
}
Why this is vulnerable:
- Reset tokens grant account access, so they must be unguessable.
- Eight hex characters provide at most 32 bits of entropy, which is brute-forceable.
- The predictable PRNG makes tokens even easier to recover.
Secure Patterns
Node.js crypto.randomBytes()
const crypto = require('crypto');
/**
* Generate cryptographically secure token (hex-encoded)
* @param {number} bytes - Number of bytes (16 = 128 bits minimum)
* @returns {string} Hex-encoded token
*/
function generateSecureToken(bytes = 16) {
return crypto.randomBytes(bytes).toString('hex');
}
/**
* Generate URL-safe base64 token
* @param {number} bytes - Number of bytes
* @returns {string} URL-safe base64 token
*/
function generateUrlSafeToken(bytes = 32) {
return crypto.randomBytes(bytes)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Generate session ID (256 bits)
function generateSessionId() {
return crypto.randomBytes(32).toString('hex'); // 64 hex chars
}
// Generate CSRF token (256 bits)
function generateCsrfToken() {
return crypto.randomBytes(32).toString('base64');
}
// Generate API key (384 bits)
function generateApiKey() {
return crypto.randomBytes(48).toString('base64');
}
// Generate password reset token (256 bits)
function generatePasswordResetToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* Generate numeric PIN with sufficient entropy
* @param {number} length - PIN length (minimum 6)
*/
function generateSecurePin(length = 6) {
if (length < 6) {
throw new Error('PIN must be at least 6 digits');
}
const digits = '0123456789';
let pin = '';
const randomBytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
pin += digits[randomBytes[i] % 10];
}
return pin;
}
// Generate UUID v4 (uses crypto.randomBytes internally)
function generateUUID() {
return crypto.randomUUID(); // Node.js 15.6+
}
// Generate encryption key for AES-256
function generateEncryptionKey() {
return crypto.randomBytes(32); // 256 bits
}
// Generate IV for AES encryption
function generateIV() {
return crypto.randomBytes(16); // 128 bits
}
Why this works:
crypto.randomBytes()uses OS-backed CSPRNG sources, not deterministic PRNGs likeMath.random().- 256-bit tokens provide enough entropy to make guessing attacks infeasible.
- Encoding (hex/base64url) preserves the underlying entropy while making values usable.
Browser crypto.getRandomValues()
/**
* Generate secure random bytes in browser
* @param {number} length - Number of bytes
* @returns {Uint8Array} Random bytes
*/
function generateRandomBytes(length) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return array;
}
// Convert Uint8Array to hex string
function toHex(bytes) {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Convert Uint8Array to base64
function toBase64(bytes) {
return btoa(String.fromCharCode.apply(null, bytes));
}
// Generate secure token in browser
function generateBrowserToken(bytes = 16) {
const randomBytes = generateRandomBytes(bytes);
return toHex(randomBytes);
}
// Generate CSRF token in browser
function generateBrowserCsrfToken() {
const randomBytes = generateRandomBytes(32);
return toBase64(randomBytes);
}
// Generate UUID v4 in browser
function generateBrowserUUID() {
return crypto.randomUUID(); // Modern browsers
}
Why this works:
crypto.getRandomValues()pulls from OS CSPRNG sources via the Web Crypto API.- 32 bytes (256 bits) of entropy is appropriate for tokens and CSRF values.
crypto.randomUUID()uses the same secure source for RFC 4122 UUIDs.
Complete encryption example with secure randomness
const crypto = require('crypto');
class SecureEncryption {
// Generate AES-256 key
static generateKey() {
return crypto.randomBytes(32); // 256 bits
}
/**
* Encrypt with AES-256-GCM (authenticated encryption)
* @param {Buffer} plaintext - Data to encrypt
* @param {Buffer} key - 32-byte encryption key
* @returns {Object} {iv, authTag, ciphertext}
*/
static encrypt(plaintext, key) {
// Generate secure IV (96 bits for GCM)
const iv = crypto.randomBytes(12);
// Create cipher
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// Encrypt
const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final()
]);
// Get authentication tag
const authTag = cipher.getAuthTag();
return { iv, authTag, ciphertext };
}
// Decrypt AES-256-GCM ciphertext
static decrypt(iv, authTag, ciphertext, key) {
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
}
}
// Usage example
const key = SecureEncryption.generateKey();
const plaintext = Buffer.from('sensitive data');
const { iv, authTag, ciphertext } = SecureEncryption.encrypt(plaintext, key);
const decrypted = SecureEncryption.decrypt(iv, authTag, ciphertext, key);
console.assert(plaintext.equals(decrypted), 'Decryption failed');
Why this works:
- Keys and IVs come from
crypto.randomBytes(), providing strong entropy for AES-256-GCM. - AES-GCM provides authenticated encryption, so tampering is detected.
- The 96-bit IV size is appropriate for GCM and is generated securely.
Framework-Specific Guidance
Express.js - Session Management
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
// SECURE - Generate strong session secret
const sessionSecret = crypto.randomBytes(64).toString('hex');
app.use(session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // HTTPS only
maxAge: 3600000, // 1 hour
sameSite: 'strict'
},
// Express-session uses crypto.randomBytes for session IDs automatically
genid: (req) => {
return crypto.randomBytes(16).toString('hex'); // Custom session ID
}
}));
// Generate CSRF tokens
const csrfTokens = new Map();
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
csrfTokens.set(req.session.csrfToken, true);
}
next();
});
Why this works:
- Session secrets and IDs are generated with
crypto.randomBytes(), providing CSPRNG entropy. - The CSRF token uses 256-bit randomness, making guessing infeasible.
Next.js - API Key Generation
// pages/api/generate-key.js
import crypto from 'crypto';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Generate secure API key
const apiKey = crypto.randomBytes(48).toString('base64');
// Hash for storage (don't store plaintext!)
const hashedKey = crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
// Store hashedKey in database
// Return apiKey to user (only shown once!)
res.status(200).json({ apiKey });
}
Why this works:
- API keys are generated from CSPRNG output, avoiding predictable tokens.
- Storing only a hash limits exposure if the database is compromised.
React - Client-side UUID Generation
import React, { useState } from 'react';
function SecureIdGenerator() {
const [id, setId] = useState('');
const generateId = () => {
// Modern browsers support crypto.randomUUID()
if (window.crypto.randomUUID) {
setId(crypto.randomUUID());
} else {
// Fallback for older browsers
const bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
const hex = Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
setId(hex);
}
};
return (
<div>
<button onClick={generateId}>Generate Secure ID</button>
<p>{id}</p>
</div>
);
}
Why this works:
crypto.randomUUID()andcrypto.getRandomValues()use secure randomness from the browser.- The fallback still uses Web Crypto, not
Math.random().
JWT Security
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// SECURE - Generate JWT signing secret
const JWT_SECRET = crypto.randomBytes(64).toString('hex');
function generateJwtToken(userId) {
// Generate unique JTI (JWT ID)
const jti = crypto.randomBytes(16).toString('hex');
return jwt.sign(
{
sub: userId,
jti: jti,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
},
JWT_SECRET,
{ algorithm: 'HS256' }
);
}
function verifyJwtToken(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (err) {
return null;
}
}
Why this works:
- The signing secret is generated with CSPRNG output, not
Math.random(). - Random
jtivalues avoid predictable identifiers in tokens.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced