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) orCryptGenRandom(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
base64urlencoding is URL-safe: Replaces+with-,/with_, omits padding for safe transmission- Independent random output: Each
randomBytes()call unpredictable, unlikeMath.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() % rangemakes 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)
base64urlencoding 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
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
base64urlencoding 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