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:
base64urlencoding 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_vssk_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: trueprevents 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:
csurfmiddleware usescrypto.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
csurfmiddleware 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