Skip to content

CWE-330: Use of Insufficiently Random Values - JavaScript/Node.js

Overview

Weak random number generation in JavaScript/Node.js occurs when developers use Math.random() for security-sensitive operations like generating session tokens, CSRF tokens, API keys, or cryptographic material. Math.random() is a pseudo-random number generator (PRNG) designed for general purposes like games and simulations, not security. It's predictable and can be influenced by timing attacks. For security purposes, use crypto.randomBytes() (Node.js) or crypto.getRandomValues() (browser).

Primary Defence: Use crypto.randomBytes() (Node.js) or crypto.getRandomValues() (browser) for all security-sensitive random value generation.

Common Vulnerable Patterns

Math.random() for Session IDs

// VULNERABLE - Predictable session ID
function generateSessionId() {
    return Math.floor(Math.random() * 1000000000).toString();
}

// Session ID like "847293561" looks random but is predictable
// Math.random() uses xorshift128+ or similar - not cryptographically secure

Why this is vulnerable: Math.random() algorithm (often xorshift128+) can be reversed. Attacker observing outputs can predict future values.

Math.random() for Reset Tokens

// VULNERABLE - Predictable password reset token
function generateResetToken() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let token = '';
    for (let i = 0; i < 32; i++) {
        token += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return token;
}

// Token like "aB7xQ9mK2pLr5tYw..." looks secure but isn't

Why this is vulnerable: Each character selection uses Math.random(). Predictable sequence = predictable token.

Date.now() as Seed

// VULNERABLE - Using timestamp as randomness
function generateToken() {
    const timestamp = Date.now();
    return timestamp.toString(36) + Math.random().toString(36).substring(2);
}

// Token like "l8x9k3a7b2c" partially based on timestamp
// Attacker knowing approximate time can narrow possibilities

Why this is vulnerable: Timestamp has limited entropy (~44 bits for millisecond timestamp). Math.random() adds weak randomness.

UUID v4 Polyfill with Math.random()

// VULNERABLE - Custom UUID implementation
function uuidv4() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;  // WEAK
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

// UUID like "f47ac10b-58cc-4372-a567-0e02b2c3d479" but predictable

Why this is vulnerable: Uses Math.random() instead of cryptographic source. Not truly random UUID.

Lottery/Gaming RNG

// VULNERABLE - Gaming random number
function rollDice() {
    return Math.floor(Math.random() * 6) + 1;  // 1-6
}

function drawLotteryNumber() {
    return Math.floor(Math.random() * 100) + 1;  // 1-100
}

// If money/rewards involved, users can predict outcomes

Why this is vulnerable: Players can predict Math.random() sequence through observation or timing attacks. Unfair advantage.

Weak Encryption IV/Nonce

const crypto = require('crypto');

// VULNERABLE - Predictable IV
function encrypt(data, key) {
    // WRONG way to generate IV
    const iv = Buffer.from(Math.random().toString(36).substring(2, 18).padEnd(16, '0'));

    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let encrypted = cipher.update(data, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    return {encrypted, iv: iv.toString('hex')};
}

// IV must be unpredictable for security

Why this is vulnerable: Predictable IV compromises encryption. Attacker can potentially decrypt.

CSRF Token with Math.random()

// VULNERABLE - Weak CSRF token
function generateCsrfToken(req, res) {
    const token = Math.random().toString(36).substring(2);  // WEAK
    req.session.csrfToken = token;
    return token;
}

// Token like "a7b3c9d2e5" - predictable

Why this is vulnerable: Attacker can predict CSRF tokens and bypass protection.

API Key Generation

// VULNERABLE - Weak API key
function generateApiKey() {
    const prefix = 'sk_';
    const random = Math.random().toString(36).substring(2) + 
                   Math.random().toString(36).substring(2);
    return prefix + random;
}

// API key like "sk_a7b3c9d2e5f1g8h4" - predictable

Why this is vulnerable: API keys grant access. Predictable keys = unauthorized access.

Math.random() for Any Security Purpose

// WRONG - All insecure
const sessionId = Math.floor(Math.random() * 1000000);
const token = Math.random().toString(36);
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    return (Math.random() * 16 | 0).toString(16);  // WEAK
});

Why this is vulnerable: Math.random() is not cryptographically secure.

Timestamp as Randomness Source

// WRONG - Low entropy
const token = Date.now().toString(36);  // ~11 chars, time-based
const random = new Date().getTime() + Math.random();

Why this is vulnerable: Timestamp predictable. Limited entropy.

Insufficient Random Bytes

const crypto = require('crypto');

// WRONG - Only 40 bits of entropy
const token = crypto.randomBytes(5).toString('hex');  // 10 hex chars

Why this is vulnerable: 40 bits = ~1 trillion possibilities. Brute-forceable.

Using Deprecated crypto.pseudoRandomBytes()

const crypto = require('crypto');

// WRONG - Deprecated, may fall back to weak PRNG
const token = crypto.pseudoRandomBytes(16).toString('hex');

Why this is vulnerable: pseudoRandomBytes() deprecated in Node.js v6. May not be cryptographically strong.

Secure Patterns

SECURE: crypto.randomBytes() for Session IDs (Node.js)

const crypto = require('crypto');

// SECURE - Cryptographically strong session ID
function generateSessionId() {
    // 32 bytes = 256 bits of entropy
    return crypto.randomBytes(32).toString('hex');
}

// Example: "3a7b9c8e4f1d2a5b6c7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9"
// Unpredictable even with knowledge of previous session IDs

Why this works:

  • crypto.randomBytes() uses OS CSPRNG: Sources entropy from /dev/urandom (Linux/Unix) or CryptGenRandom (Windows)
  • Hardware-backed randomness: OS collects entropy from keyboard timing, disk I/O, network interrupts
  • Unpredictable output: Unlike Math.random() (xorshift128+), cannot be predicted from previous outputs
  • 256 bits provides brute-force resistance: 2^256 possibilities makes attacks computationally infeasible
  • Hex encoding for compatibility: 64-character string safe for cookies, URLs, database storage

SECURE: crypto.randomBytes() for Reset Tokens

const crypto = require('crypto');

// SECURE - Password reset token
function generateResetToken() {
    // 32 bytes = 256 bits, base64-encoded
    return crypto.randomBytes(32).toString('base64url');
}

// Example: "A3b7K9xQmZpLr4tYwFj2nVc8hG1sE6uD..."
// URL-safe, unpredictable

Why this works:

  • Password reset tokens require high security: Grant temporary access to change account credentials
  • 256 bits prevents brute-force: 2^256 token space makes guessing impossible in reasonable timeframe
  • base64url encoding is URL-safe: Replaces + with -, / with _, omits padding for safe transmission
  • Independent random output: Each randomBytes() call unpredictable, unlike Math.random() where observation enables prediction
  • Tokens should be single-use and time-limited: Expire after 1 hour for defense-in-depth

SECURE: crypto.randomInt() for Random Integers

const crypto = require('crypto');

// SECURE - Random integer in range
function generateVerificationCode() {
    // 6-digit code: 000000 to 999999
    const code = crypto.randomInt(0, 1000000);
    return code.toString().padStart(6, '0');
}

// Example: "047382"
// Uniformly distributed, cryptographically random

Why this works:

  • crypto.randomInt() uses rejection sampling: Avoids modulo bias for uniform distribution across ranges
  • Prevents statistical bias: Naive randomBytes() % range makes some numbers appear more frequently
  • Perfectly uniform distribution: Discards out-of-range values and retries until valid
  • Cryptographically secure: Suitable for 2FA, email verification, SMS confirmation codes
  • Resistant to brute-force: Cannot be predicted or guessed within typical expiration windows (5-10 minutes)

SECURE: crypto.randomUUID() for UUIDs

const crypto = require('crypto');

// SECURE - UUID v4 generation (Node.js 14.17+, 15.6+)
function generateUserId() {
    return crypto.randomUUID();
}

- **RFC 4122 version 4 UUIDs with 122 bits randomness:** Version/variant bits fixed per RFC, rest cryptographically random
- **Sufficient uniqueness for distributed systems:** No coordination needed, astronomically low collision probability
- **Superior to Math.random()-based UUIDs:** Uses `crypto.randomBytes()` for entropy, not weak PRNGs
- **Universal database support:** 8-4-4-4-12 format recognized by all major databases
- **Suitable for high-throughput systems:** Millions of UUIDs/second with negligible collision risk

// Example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// Cryptographically secure UUID v4

SECURE: crypto.getRandomValues() in Browser

// SECURE - Browser-side random values
function generateClientToken() {
    // Uint8Array of 32 random bytes
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);

    // Convert to hex string
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}

// Example: "3a7b9c8e4f1d2a5b6c7e8f9a0b1c2d3e..."
// Cryptographically secure in modern browsers

Why this works:

  • Browser equivalent of crypto.randomBytes(): Provides access to browser's CSPRNG
  • Platform-specific CSPRNGs: Uses /dev/urandom (Linux), CryptGenRandom/BCryptGenRandom (Windows), SecRandomCopyBytes (macOS/iOS)
  • Fills TypedArrays with random values: Can generate up to 65,536 bytes per call
  • Essential for client-side security: OAuth client secrets, CSP nonces, WebAuthn challenges, E2E encryption keys
  • Universal browser support: Chrome, Firefox, Safari, Edge; throws exception in insecure contexts (non-HTTPS)

SECURE: Encryption IV/Nonce Generation

const crypto = require('crypto');

// SECURE - Proper IV generation
function encrypt(data, key) {
    // 16 bytes IV for AES-256-CBC
    const iv = crypto.randomBytes(16);

    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let encrypted = cipher.update(data, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    return {
        encrypted,
        iv: iv.toString('hex')
    };
}

// IV is unpredictable and unique

Why this works:

  • IVs/nonces prevent pattern analysis: Same plaintext produces different ciphertexts each encryption
  • 16-byte IV must be unpredictable for AES-CBC: Predictable IVs enable chosen-plaintext attacks
  • crypto.randomBytes() ensures independence: Each IV unpredictable from OS CSPRNG
  • IV doesn't need secrecy but requires uniqueness: Stored with ciphertext; reuse with same key is catastrophic
  • Collision probability negligible: 1 in 2^128 for 16-byte IVs
  • AES-GCM nonce reuse breaks security: 12-byte nonce recommended; reuse allows message forgery

SECURE: CSRF Token Generation

const crypto = require('crypto');

// SECURE - Strong CSRF token
function generateCsrfToken(req, res) {
    const token = crypto.randomBytes(32).toString('base64');
    req.session.csrfToken = token;
    return token;
}

// Token like "A7b3C9d2E5f1G8h4..."
// Cryptographically unpredictable

Why this works:

  • CSRF protection relies on unpredictability: Attackers from third-party sites cannot guess token values
  • 256 bits prevents brute-force: 1 billion guesses/second still takes longer than universe's age
  • base64 encoding for form/header compatibility: Compact string for hidden fields or HTTP headers
  • Server-side storage enables validation: Store in session or signed cookie; validate on state-changing requests
  • Cryptographic randomness prevents prediction: Observing thousands of tokens doesn't reveal generation algorithm
  • Superior to predictable schemes: Unlike hashing session IDs which could be reversed

SECURE: API Key Generation

const crypto = require('crypto');

// SECURE - Cryptographically strong API key
function generateApiKey() {
    const prefix = 'sk_live_';
    const randomPart = crypto.randomBytes(24).toString('base64url');
    return prefix + randomPart;
}

// Example: "sk_live_A7b3K9xQmZpLr4tYwFj2nVc8..."
// 192 bits of entropy in random part

Why this works:

  • 192 bits entropy prevents brute-force: 1 billion guesses/second takes ~2^160 seconds (far beyond universe's age)
  • base64url encoding is URL-safe: Suitable for HTTP headers, query parameters, config files
  • Prefix identifies key type: sk_live_ enables pattern matching, rate limiting, revocation by type
  • One key compromise doesn't affect others: Cryptographic randomness prevents key prediction/derivation
  • Production best practices: Implement key rotation, scope limiting, audit logging

Key Security Functions

Secure Token Generator

const crypto = require('crypto');

class SecureTokenGenerator {
    /**
     * Generate secure tokens for various purposes
     * @param {string} purpose - 'session', 'reset', 'api', 'csrf'
     * @param {number} bytes - Number of random bytes (default 32)
     * @returns {string} Secure token
     */
    static generate(purpose, bytes = 32) {
        const token = crypto.randomBytes(bytes).toString('base64url');
        return `${purpose}_${token}`;
    }

    static session() {
        return this.generate('sess', 32);  // 256 bits
    }

    static resetPassword() {
        return this.generate('reset', 48);  // 384 bits (longer for password reset)
    }

    static apiKey() {
        return this.generate('sk_live', 32);
    }

    static csrf() {
        return this.generate('csrf', 24);  // 192 bits
    }
}

// Usage
const sessionToken = SecureTokenGenerator.session();
const resetToken = SecureTokenGenerator.resetPassword();

Entropy Estimator

function estimateEntropy(value, alphabetSize) {
    /**
     * Estimate entropy bits for a random value
     * @param {string} value - The random value
     * @param {number} alphabetSize - Character set size (e.g., 64 for base64)
     * @returns {number} Estimated entropy in bits
     */
    const length = value.length;
    const entropyBits = length * Math.log2(alphabetSize);
    return entropyBits;
}

// Usage
const token = crypto.randomBytes(32).toString('base64');  // 44 chars
const entropy = estimateEntropy(token, 64);  // 264 bits
console.log(`Token entropy: ${entropy} bits`);

// Minimum entropy recommendations:
// - Session tokens: 128 bits
// - Password reset: 128-256 bits
// - API keys: 128-256 bits
// - Encryption keys: 128-256 bits

Secure Random String Generator

const crypto = require('crypto');

function generateSecureString(length, charset = 'alphanumeric') {
    /**
     * Generate cryptographically secure random string
     * @param {number} length - Desired string length
     * @param {string} charset - 'alphanumeric', 'hex', 'base64', 'numeric'
     * @returns {string} Random string
     */
    const charsets = {
        alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
        hex: '0123456789abcdef',
        base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
        numeric: '0123456789'
    };

    const chars = charsets[charset] || charsets.alphanumeric;
    const bytes = crypto.randomBytes(length);

    let result = '';
    for (let i = 0; i < length; i++) {
        result += chars[bytes[i] % chars.length];
    }

    return result;
}

// Usage
const apiKey = generateSecureString(32, 'alphanumeric');
const pin = generateSecureString(6, 'numeric');
const hexToken = generateSecureString(64, 'hex');

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

Analysis Steps

Locate the weak random usage

// Line 23 in src/auth/tokens.js
function generatePasswordResetToken(userId) {
    const token = Math.random().toString(36).substring(2);  // VULNERABLE
    return `${userId}_${token}`;
}

Identify the purpose

  • Password reset token generation (critical security operation)
  • Used for account recovery
  • Predictable token = account takeover

Assess the risk

  • Token grants access to change password
  • Attacker can predict tokens for target user
  • Impact: Critical (full account takeover)

Calculate current entropy

// token = Math.random().toString(36).substring(2)
// toString(36) produces ~11 chars from Math.random()
// Alphabet: 36 chars (0-9, a-z)
// Entropy: 11 * log2(36) ≈ 57 bits
// INSUFFICIENT for security (need 128+ bits)

Remediation Steps

Replace Math.random() with crypto.randomBytes()

// BEFORE (Line 23 - vulnerable)
function generatePasswordResetToken(userId) {
    const token = Math.random().toString(36).substring(2);
    return `${userId}_${token}`;
}

// AFTER (fixed)
const crypto = require('crypto');

function generatePasswordResetToken(userId) {
    const token = crypto.randomBytes(32).toString('base64url');
    return `${userId}_${token}`;
}

Add token expiration

const crypto = require('crypto');

function generatePasswordResetToken(userId) {
    const token = crypto.randomBytes(32).toString('base64url');
    const expiry = Date.now() + (3600 * 1000);  // 1 hour

    // Store token with expiry in database
    storeResetToken(userId, token, expiry);

    return token;
}

Invalidate existing tokens

// After deploying fix, invalidate all existing reset tokens
// since they were generated with weak random
await db.query('DELETE FROM password_reset_tokens WHERE created_at < NOW()');

Re-scan

npm run lint:security
# Or run ESLint with security plugin

Security Checklist

  • Never use Math.random() for security (sessions, tokens, keys, passwords)
  • Use crypto.randomBytes() (Node.js) for all security-critical values
  • Use crypto.getRandomValues() (browser) for client-side randomness
  • Use crypto.randomInt() for random integers (Node.js 14.10+)
  • Use crypto.randomUUID() for UUIDs (Node.js 14.17+)
  • Ensure minimum 128 bits entropy for security-critical tokens
  • Never use timestamps or PIDs as randomness source
  • Use base64url encoding for URL-safe tokens
  • Set token expiration times
  • Run ESLint security plugin to detect Math.random()
  • Test token uniqueness in unit tests
  • Invalidate existing tokens if weak random was used

Additional Resources