Skip to content

CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) - JavaScript/Node.js

Overview

Use of Cryptographically Weak PRNG in JavaScript/Node.js applications occurs when developers use Math.random() for security-sensitive operations. Math.random() is not cryptographically secure and should never be used for generating tokens, keys, passwords, or other security-critical values. Attackers can exploit predictable random values to compromise sessions, guess tokens, or break encryption.

Primary Defence: Use crypto.randomBytes() or crypto.randomUUID() (Node.js 14.17+) for all security-sensitive random value generation including session tokens, CSRF tokens, and API keys.

Common Vulnerable Patterns

Math.random() for Session Tokens

// VULNERABLE - Using Math.random() for session tokens
function generateWeakSessionToken() {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let token = '';

  for (let i = 0; i < 32; i++) {
    const randomIndex = Math.floor(Math.random() * chars.length);
    token += chars[randomIndex];
  }

  return token;
}

// Express route with vulnerable session generation
const express = require('express');
const session = require('express-session');

const app = express();

app.use(session({
  genid: () => generateWeakSessionToken(),  // VULNERABLE!
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}));

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  if (authenticateUser(username, password)) {
    // VULNERABLE - Predictable session ID
    req.session.userId = getUserId(username);
    req.session.token = generateWeakSessionToken();

    res.json({
      status: 'logged_in',
      sessionToken: req.session.token
    });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

function authenticateUser(username, password) {
  // Auth logic
  return true;
}

function getUserId(username) {
  return 123;
}

Why this is vulnerable:

  • Math.random() is predictable (uses weak PRNG)
  • Session tokens can be guessed
  • Enables session hijacking
  • Not suitable for cryptographic purposes

Weak API Key Generation

// VULNERABLE - Math.random() for API keys
function generateWeakAPIKey() {
  const prefix = 'sk_';
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  let key = prefix;

  for (let i = 0; i < 32; i++) {
    key += chars[Math.floor(Math.random() * chars.length)];
  }

  return key;
}

// Fastify route with weak API key generation
const fastify = require('fastify')({ logger: true });

fastify.post('/api/keys', async (request, reply) => {
  const { userId, description } = request.body;

  // VULNERABLE - Predictable API key
  const apiKey = generateWeakAPIKey();

  // Store in database
  await db.apiKeys.insert({
    userId,
    apiKey,
    description,
    createdAt: new Date()
  });

  return {
    apiKey,
    message: 'Store this key securely'
  };
});

Why this is vulnerable:

  • API keys are predictable
  • Attackers can guess keys for other users
  • Compromises API security
  • Enables unauthorized access

Weak Password Reset Tokens

// VULNERABLE - Weak token generation
function generateWeakResetToken() {
  // Using timestamp and Math.random() - both predictable!
  const timestamp = Date.now().toString();
  const randomPart = Math.random().toString(36).substring(2);

  return timestamp + randomPart;
}

// Next.js API route with weak reset tokens
export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { email } = req.body;

    // VULNERABLE - Predictable reset token
    const resetToken = generateWeakResetToken();

    // Store in database
    await db.passwordResets.create({
      email,
      token: resetToken,
      expiresAt: new Date(Date.now() + 3600000)  // 1 hour
    });

    // Send email
    await sendPasswordResetEmail(email, resetToken);

    res.status(200).json({ message: 'Reset email sent' });
  }
}

async function sendPasswordResetEmail(email, token) {
  // Email sending logic
}

Why this is vulnerable:

  • Timestamp is guessable
  • Math.random() is predictable
  • Attackers can predict reset tokens
  • Enables account takeover

Weak UUID Generation

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

// Express route with weak UUIDs
const express = require('express');
const app = express();

app.post('/api/users', async (req, res) => {
  const { username, email } = req.body;

  // VULNERABLE - Predictable user IDs
  const userId = generateWeakUUID();

  await db.users.create({
    id: userId,
    username,
    email
  });

  res.json({
    userId,
    username
  });
});

Why this is vulnerable:

  • UUIDs are predictable
  • Enables user enumeration
  • Privacy violation
  • Attackers can guess valid user IDs

Weak CSRF Token

// VULNERABLE - Math.random() for CSRF tokens
function generateWeakCSRFToken() {
  return Math.random().toString(36).substring(2) +
         Math.random().toString(36).substring(2);
}

// Express middleware with weak CSRF
const express = require('express');
const app = express();

app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    // VULNERABLE - Weak CSRF token
    req.session.csrfToken = generateWeakCSRFToken();
  }
  next();
});

app.get('/form', (req, res) => {
  res.render('form', {
    csrfToken: req.session.csrfToken
  });
});

app.post('/submit', (req, res) => {
  const userToken = req.body.csrfToken;
  const sessionToken = req.session.csrfToken;

  if (userToken !== sessionToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  // Process form
  res.json({ status: 'success' });
});

Why this is vulnerable:

  • CSRF tokens must be unpredictable
  • Math.random() allows prediction
  • Defeats CSRF protection
  • Enables CSRF attacks

Weak IV Generation

const crypto = require('crypto');

// VULNERABLE - Weak IV using Math.random()
function weakEncrypt(plaintext, key) {
  // WRONG: Using Math.random() for IV
  const iv = Buffer.from(
    Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))
  );

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

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

// Express route using weak encryption
const express = require('express');
const app = express();

const ENCRYPTION_KEY = crypto.randomBytes(32);

app.post('/api/encrypt', express.json(), (req, res) => {
  const { data } = req.body;

  // VULNERABLE - Weak IV compromises encryption
  const encrypted = weakEncrypt(data, ENCRYPTION_KEY);

  res.json({ encrypted });
});

Why this is vulnerable:

  • IV must be unpredictable for CBC mode
  • Weak IV allows plaintext recovery
  • Compromises confidentiality
  • Enables chosen-plaintext attacks

Weak OTP Generation

// VULNERABLE - Math.random() for OTP
function generateWeakOTP() {
  return Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
}

// NestJS controller with weak OTP
import { Controller, Post, Body } from '@nestjs/common';

@Controller('api/auth')
export class AuthController {
  @Post('send-otp')
  async sendOTP(@Body() body: { phoneNumber: string }) {
    // VULNERABLE - Predictable OTP
    const otp = generateWeakOTP();

    // Store OTP
    await this.otpService.saveOTP(body.phoneNumber, otp);

    // Send SMS
    await this.smsService.send(body.phoneNumber, `Your OTP is: ${otp}`);

    return { message: 'OTP sent' };
  }

  @Post('verify-otp')
  async verifyOTP(@Body() body: { phoneNumber: string, otp: string }) {
    const valid = await this.otpService.verify(body.phoneNumber, body.otp);

    if (!valid) {
      return { error: 'Invalid OTP' };
    }

    return { status: 'verified' };
  }
}

Why this is vulnerable:

  • OTPs are predictable
  • Only 1 million possibilities (easily brute-forced)
  • Attackers can guess OTPs
  • Compromises 2FA security

Weak Password Generation

// VULNERABLE - Math.random() for password generation
function generateWeakPassword(length = 12) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
  let password = '';

  for (let i = 0; i < length; i++) {
    password += chars[Math.floor(Math.random() * chars.length)];
  }

  return password;
}

// Express route with weak password generation
const express = require('express');
const app = express();

app.post('/api/users/reset-password', async (req, res) => {
  const { email } = req.body;

  // VULNERABLE - Weak temporary password
  const tempPassword = generateWeakPassword(12);

  // Update user password
  await db.users.update(
    { email },
    { password: hashPassword(tempPassword) }
  );

  // Send email
  await sendEmail(email, `Your temporary password is: ${tempPassword}`);

  res.json({ message: 'Temporary password sent' });
});

function hashPassword(password) {
  // Password hashing logic
  return password;
}

async function sendEmail(email, message) {
  // Email sending logic
}

Why this is vulnerable:

  • Temporary passwords are predictable
  • Attackers can guess passwords
  • Compromises account security
  • Defeats password security

Secure Patterns

Using crypto.randomBytes() for Tokens

const crypto = require('crypto');

function generateSecureSessionToken() {
  // SECURE - Using crypto.randomBytes()
  return crypto.randomBytes(32).toString('base64url');
}

function generateHexToken() {
  // Alternative: Hex encoding
  return crypto.randomBytes(32).toString('hex');
}

// Express with secure session tokens
const express = require('express');
const session = require('express-session');

const app = express();

// SECURE - Random secret key
const SESSION_SECRET = crypto.randomBytes(64).toString('hex');

app.use(session({
  genid: () => generateSecureSessionToken(),
  secret: SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,  // HTTPS only
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  }
}));

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  if (await authenticateUser(username, password)) {
    // SECURE - Cryptographically strong session token
    req.session.userId = await getUserId(username);
    req.session.regenerate((err) => {
      if (err) {
        return res.status(500).json({ error: 'Session error' });
      }

      res.json({
        status: 'logged_in',
        sessionId: req.sessionID
      });
    });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

async function authenticateUser(username, password) {
  // Secure authentication logic
  return true;
}

async function getUserId(username) {
  return 123;
}

module.exports = app;

Why this works:

  • Cryptographically secure source: Node.js's crypto.randomBytes() uses OS-level CSPRNGs (OpenSSL's RAND_bytes: /dev/urandom on Linux, CryptGenRandom on Windows) providing unpredictable entropy
  • 256 bits of entropy: 32-byte tokens yield 2^256 possible values, making brute-force computationally impossible
  • URL-safe encoding: base64url encoding replaces + with -, / with _, removes padding for safe transmission in URLs/headers/cookies
  • Compact format: More efficient than hex (43 vs 64 characters for 32 bytes) while preserving full entropy
  • Security-critical use cases: Appropriate for session tokens, API keys, CSRF tokens, and any identifier requiring cryptographic unpredictability

Secure API Key Generation

const crypto = require('crypto');

class SecureAPIKeyService {
  static generateAPIKey() {
    // SECURE - Cryptographically strong API key
    const randomBytes = crypto.randomBytes(32);
    return 'sk_live_' + randomBytes.toString('base64url');
  }

  static hashAPIKey(apiKey) {
    // Hash for storage (never store plaintext)
    return crypto.createHash('sha256')
      .update(apiKey)
      .digest('hex');
  }

  static async createAPIKey(userId, description) {
    const apiKey = this.generateAPIKey();
    const keyHash = this.hashAPIKey(apiKey);

    // Store hash in database
    await db.apiKeys.insert({
      userId,
      keyHash,  // Store hash, not plaintext
      description,
      createdAt: new Date(),
      lastUsed: null
    });

    // Return plaintext key only once
    return apiKey;
  }

  static async verifyAPIKey(apiKey) {
    const keyHash = this.hashAPIKey(apiKey);

    const key = await db.apiKeys.findOne({
      keyHash,
      revoked: false
    });

    if (!key) {
      return null;
    }

    // Update last used
    await db.apiKeys.update(
      { _id: key._id },
      { $set: { lastUsed: new Date() } }
    );

    return key;
  }
}

// Fastify route with secure API keys
const fastify = require('fastify')({ logger: true });

fastify.post('/api/keys', async (request, reply) => {
  const { userId, description } = request.body;

  // SECURE - Cryptographically strong API key
  const apiKey = await SecureAPIKeyService.createAPIKey(userId, description);

  return {
    apiKey,
    warning: 'Store this key securely. It will not be shown again.'
  };
});

fastify.get('/api/protected', {
  preHandler: async (request, reply) => {
    const authHeader = request.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      reply.code(401).send({ error: 'Missing API key' });
      return;
    }

    const apiKey = authHeader.substring(7);
    const key = await SecureAPIKeyService.verifyAPIKey(apiKey);

    if (!key) {
      reply.code(401).send({ error: 'Invalid API key' });
      return;
    }

    request.userId = key.userId;
  }
}, async (request, reply) => {
  return {
    message: 'Access granted',
    userId: request.userId
  };
});

module.exports = fastify;

Why this works:

  • 256-bit cryptographic entropy: crypto.randomBytes(32) makes keys globally unique and impossible to predict
  • Hash before storage: SHA-256 hashing prevents exposure of working keys if database is compromised
  • One-time display: Returning plaintext key only during creation enforces secure storage by users
  • Industry-standard prefix: sk_live_ naming (Stripe convention) enables automated scanning for accidental commits and distinguishes key types (sk_test_ vs sk_live_)
  • Defense-in-depth: Combination of strong generation, hashing, single display, and conventional naming provides layered security

Secure Password Reset Tokens

const crypto = require('crypto');

class SecurePasswordResetService {
  static generateResetToken() {
    // SECURE - 32 bytes of cryptographically secure random data
    return crypto.randomBytes(32).toString('base64url');
  }

  static async initiateReset(email) {
    const resetToken = this.generateResetToken();

    // Store token with expiration
    await db.passwordResets.insert({
      email,
      token: resetToken,
      createdAt: new Date(),
      expiresAt: new Date(Date.now() + 3600000),  // 1 hour
      used: false
    });

    // Send email
    await this.sendResetEmail(email, resetToken);

    return { message: 'Reset email sent' };
  }

  static async resetPassword(token, newPassword) {
    const reset = await db.passwordResets.findOne({
      token,
      used: false,
      expiresAt: { $gt: new Date() }
    });

    if (!reset) {
      throw new Error('Invalid or expired token');
    }

    // Update password
    await db.users.update(
      { email: reset.email },
      { password: await hashPassword(newPassword) }
    );

    // Mark token as used
    await db.passwordResets.update(
      { _id: reset._id },
      { $set: { used: true } }
    );

    return { message: 'Password reset successful' };
  }

  static async sendResetEmail(email, token) {
    const resetUrl = `https://example.com/reset-password?token=${token}`;

    // Send email (using your email service)
    await emailService.send({
      to: email,
      subject: 'Password Reset Request',
      text: `Click here to reset your password: ${resetUrl}\n\nThis link expires in 1 hour.`
    });
  }
}

// Next.js API route with secure reset tokens
export default async function handler(req, res) {
  if (req.method === 'POST' && req.url === '/api/reset-password') {
    const { email } = req.body;

    try {
      // SECURE - Cryptographically strong reset token
      await SecurePasswordResetService.initiateReset(email);
      res.status(200).json({ message: 'Reset email sent' });
    } catch (error) {
      res.status(500).json({ error: 'Reset failed' });
    }
  } else if (req.method === 'POST' && req.url === '/api/reset-password/confirm') {
    const { token, newPassword } = req.body;

    try {
      await SecurePasswordResetService.resetPassword(token, newPassword);
      res.status(200).json({ message: 'Password reset successful' });
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
}

async function hashPassword(password) {
  const bcrypt = require('bcrypt');
  return await bcrypt.hash(password, 12);
}

Why this works:

  • Cryptographic unpredictability: 256-bit entropy from crypto.randomBytes(32) prevents guessing even with email/timing knowledge
  • One-time use: Marking tokens used: true prevents replay attacks from intercepted reset emails
  • Time-limited window: Short expiration (typically 1 hour) limits exploitation even if token is compromised
  • Automatic expiration: Database query with timestamp check rejects stale tokens without manual cleanup
  • Proof of ownership: More secure than temporary passwords - requires email access to set new password
  • Defense-in-depth: Cryptographic randomness + single-use + expiration creates multiple protective layers

Using crypto.randomUUID()

const crypto = require('crypto');

// SECURE - crypto.randomUUID() (Node.js 14.17+)
function generateSecureUUID() {
  return crypto.randomUUID();
}

// Express route with secure UUIDs
const express = require('express');
const app = express();
app.use(express.json());

app.post('/api/users', async (req, res) => {
  const { username, email } = req.body;

  // SECURE - Cryptographically strong UUID
  const userId = crypto.randomUUID();

  await db.users.create({
    id: userId,
    username,
    email,
    createdAt: new Date()
  });

  res.json({
    userId,
    username
  });
});

// Alternative: Secure custom ID generation
function generateSecureId(prefix = 'id_') {
  const randomPart = crypto.randomBytes(16).toString('base64url');
  return prefix + randomPart;
}

module.exports = app;

Why this works:

  • 122 bits of randomness: RFC 4122 v4 UUIDs use crypto.randomBytes() internally (6 bits for version/variant), sufficient to prevent collisions even with trillions generated
  • Prevents enumeration attacks: Unpredictability stops attackers from discovering resources via sequential ID incrementing
  • Ideal for public identifiers: Perfect for REST API resources, URLs, job queue IDs, file names where sequential patterns leak information
  • Wide interoperability: Standardized 8-4-4-4-12 hex format recognized across databases, APIs, and languages
  • Security consideration: For session tokens or API secrets, prefer explicit 256-bit tokens via crypto.randomBytes()
  • Balance: UUIDs provide security, global uniqueness, and broad compatibility

Secure CSRF Token

const crypto = require('crypto');

function generateSecureCSRFToken() {
  // SECURE - Cryptographically strong CSRF token
  return crypto.randomBytes(32).toString('base64url');
}

// Express with secure CSRF protection
const express = require('express');
const csrf = require('csurf');

const app = express();

// Use csurf middleware (uses crypto.randomBytes internally)
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  }
});

app.use(csrfProtection);

app.get('/form', (req, res) => {
  // SECURE - CSRF token from csurf middleware
  res.render('form', {
    csrfToken: req.csrfToken()
  });
});

app.post('/submit', (req, res) => {
  // CSRF validation happens automatically
  // Process form
  res.json({ status: 'success' });
});

// Manual CSRF implementation (if not using middleware)
class SecureCSRFService {
  static generateToken() {
    return crypto.randomBytes(32).toString('base64url');
  }

  static verifyToken(userToken, storedToken) {
    if (!userToken || !storedToken) {
      return false;
    }

    // Timing-safe comparison
    return crypto.timingSafeEqual(
      Buffer.from(userToken),
      Buffer.from(storedToken)
    );
  }
}

module.exports = app;

Why this works:

  • Unpredictable tokens: csurf middleware uses crypto.randomBytes() for 256-bit tokens preventing guessing/forgery
  • Timing-safe comparison: crypto.timingSafeEqual() provides constant-time verification preventing bit-by-bit timing attacks
  • Session binding: Tokens stored in session and required in POST/PUT/DELETE bind to authenticated user, readable only by same-origin
  • Double-submit pattern: Token in both cookie and request body provides defense without session storage
  • Automatic handling: Express csurf middleware manages generation, validation, rotation reducing implementation errors
  • Essential protection: Required for any application with authenticated state-changing operations

Secure IV and Salt Generation

const crypto = require('crypto');

class SecureEncryption {
  constructor(masterKey) {
    this.masterKey = masterKey;
  }

  static generateKey() {
    // SECURE - Generate encryption key
    return crypto.randomBytes(32);  // 256 bits for AES-256
  }

  static generateSalt() {
    // SECURE - Generate salt for key derivation
    return crypto.randomBytes(32);  // 256 bits
  }

  encrypt(plaintext) {
    // SECURE - Generate cryptographically strong IV
    const iv = crypto.randomBytes(16);  // 128 bits for AES

    const cipher = crypto.createCipheriv('aes-256-gcm', this.masterKey, iv);

    let encrypted = cipher.update(plaintext, 'utf8');
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    const authTag = cipher.getAuthTag();

    // Combine IV + auth tag + ciphertext
    const combined = Buffer.concat([iv, authTag, encrypted]);

    return combined.toString('base64');
  }

  decrypt(encryptedData) {
    const combined = Buffer.from(encryptedData, 'base64');

    // Extract components
    const iv = combined.slice(0, 16);
    const authTag = combined.slice(16, 32);
    const encrypted = combined.slice(32);

    const decipher = crypto.createDecipheriv('aes-256-gcm', this.masterKey, iv);
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(encrypted);
    decrypted = Buffer.concat([decrypted, decipher.final()]);

    return decrypted.toString('utf8');
  }
}

// Express route using secure encryption
const express = require('express');
const app = express();
app.use(express.json());

// Generate master key (store securely, e.g., environment variable)
const MASTER_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'base64');
const encryptor = new SecureEncryption(MASTER_KEY);

app.post('/api/encrypt', (req, res) => {
  const { data } = req.body;

  // SECURE - Uses crypto.randomBytes() for IV
  const encrypted = encryptor.encrypt(data);

  res.json({ encrypted });
});

app.post('/api/decrypt', (req, res) => {
  const { encrypted } = req.body;

  try {
    const decrypted = encryptor.decrypt(encrypted);
    res.json({ decrypted });
  } catch (error) {
    res.status(400).json({ error: 'Decryption failed' });
  }
});

module.exports = app;

Why this works:

  • Unique random IVs: crypto.randomBytes(16) generates cryptographically random 128-bit IVs; reusing IVs with same key catastrophically breaks confidentiality via XOR analysis
  • Authenticated encryption: AES-GCM encrypts and generates 16-byte authentication tag detecting tampering, preventing ciphertext manipulation
  • IV uniqueness requirement: IV must be unique per encryption (not secret) - typically stored/transmitted with ciphertext
  • Integrity guarantee: Authentication tag ensures even single-bit modifications cause decryption failure, not corrupted plaintext
  • Proper cryptographic practice: Combines random IVs, authenticated mode (GCM), correct IV/tag/ciphertext handling
  • Critical warning: Never hardcode, reuse, or use sequential/timestamp-based IVs

Secure OTP Generation

const crypto = require('crypto');

class SecureOTPService {
  static generateNumericOTP(length = 6) {
    // SECURE - Cryptographically strong numeric OTP
    let otp = '';

    for (let i = 0; i < length; i++) {
      // Use crypto.randomInt() for secure random integers
      otp += crypto.randomInt(0, 10);
    }

    return otp;
  }

  static generateAlphanumericOTP(length = 8) {
    // SECURE - Alphanumeric OTP
    const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';  // Exclude ambiguous chars
    let otp = '';

    for (let i = 0; i < length; i++) {
      const randomIndex = crypto.randomInt(0, chars.length);
      otp += chars[randomIndex];
    }

    return otp;
  }

  static async createOTP(identifier, type = 'numeric') {
    const code = type === 'numeric' ?
      this.generateNumericOTP(6) :
      this.generateAlphanumericOTP(8);

    await db.otps.insert({
      identifier,
      code,
      type,
      createdAt: new Date(),
      expiresAt: new Date(Date.now() + 600000),  // 10 minutes
      used: false
    });

    return code;
  }

  static async verifyOTP(identifier, code) {
    const otp = await db.otps.findOne({
      identifier,
      code,
      used: false,
      expiresAt: { $gt: new Date() }
    });

    if (!otp) {
      return false;
    }

    // Mark as used
    await db.otps.update(
      { _id: otp._id },
      { $set: { used: true } }
    );

    return true;
  }
}

// NestJS controller with secure OTP
import { Controller, Post, Body } from '@nestjs/common';

@Controller('api/auth')
export class AuthController {
  @Post('send-otp')
  async sendOTP(@Body() body: { phoneNumber: string }) {
    // SECURE - Cryptographically strong OTP
    const otp = await SecureOTPService.createOTP(body.phoneNumber);

    // Send SMS
    await smsService.send(body.phoneNumber, `Your OTP is: ${otp}`);

    return { message: 'OTP sent' };
  }

  @Post('verify-otp')
  async verifyOTP(@Body() body: { phoneNumber: string, otp: string }) {
    const valid = await SecureOTPService.verifyOTP(body.phoneNumber, body.otp);

    if (!valid) {
      return { error: 'Invalid or expired OTP' };
    }

    return { status: 'verified' };
  }
}

Why this works:

  • Uniform cryptographic randomness: crypto.randomInt(0, 1000000) generates 6-digit OTPs with uniform distribution (1 million possibilities, 500K average brute-force attempts)
  • Layered defenses: One-time use + short expiration (5-10 min) + rate limiting (3-5 attempts) makes brute-force practically impossible
  • Prevents prediction: Cryptographic strength ensures future OTPs unpredictable from past samples, unlike weak PRNGs
  • Automatic cleanup: Timestamp and used status enable expiration management and prevent replay attacks
  • Security scaling: 8-digit OTPs (100M combinations) or alphanumeric (6 chars from 36 = 2.1B) for higher security
  • Balanced approach: Suitable for email/SMS 2FA, temporary codes where convenience balances security

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