Skip to content

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 like Math.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() and crypto.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 jti values 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

Additional Resources