Description
Using Math.random() for reset tokens, session IDs, OTPs or CSRF tokens provides no cryptographic guarantee against prediction. Depending on the implementation and observable output, an attacker may predict later values. A small value space also makes guessing easier. Security-sensitive random values need sufficient length and a cryptographically secure generator.
Potential impact
- Guessable tokens or session IDs may allow account access or session hijacking.
- An attacker may guess a reset token and change the account's password.
- Predictable OTPs may undermine an additional authentication step.
- Predictable CSRF tokens may fail to prevent forged requests.
- Predictable keys or violations of an algorithm's nonce or IV requirements may weaken confidentiality or integrity.
Remediation
- Use a cryptographically secure pseudorandom number generator (CSPRNG), such as browser
window.crypto.getRandomValues, Node.jscrypto.randomBytes, orcrypto.randomIntfor numeric codes. - Give random tokens enough entropy to resist guessing, for example at least 128 bits. Follow the algorithm's length, uniqueness and unpredictability requirements for keys, nonces and IVs. Short OTPs also need expiration and attempt limits.
- Encode bytes as hex or base64url. When mapping them to a custom alphabet, avoid modulo bias, for example through rejection sampling.
- Do not use
Math.random()for security-sensitive passwords, tokens, keys, nonces or OTPs.
Examples
Before
// BAD: Use Math.random() for a security-sensitive token
function createResetToken() {
// Predictable output with limited entropy
const token =
Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
return token; // Unsuitable for reset tokens, sessions or API keys
}
After
// GOOD (Node.js): CSPRNG for a 256-bit token
const crypto = require("crypto");
function createResetToken() {
return crypto.randomBytes(32).toString("hex"); // 64 hexadecimal characters, without encoding bias
}
// GOOD (Browser): Web Crypto API
function createCsrfToken() {
const bytes = crypto.getRandomValues(new Uint8Array(32)); // 256 bits
// Encode as base64url
let b64 = btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
return b64;
}
Converting Math.random() output to a string does not add cryptographic security. The Node.js and browser portions of the second example run separately in their respective environments. Each encodes 32 generated bytes as hex or base64url. Also restrict a token's purpose, expiration and permitted uses, and protect its storage and transmission.