Quick Answer
Math.random() uses a predictable pseudorandom number generator - given enough observed outputs, an attacker can predict future values. When AI tools use it to generate password reset tokens, session IDs, or invitation codes, those values become guessable. The fix is one line: replace Math.random() with crypto.randomUUID() or crypto.getRandomValues().
The Problem With "Random" That Isn't Cryptographically Random
There are two categories of random number generators in computing: pseudorandom (PRNG) and cryptographically secure pseudorandom (CSPRNG). The difference is not about how "random-looking" the output is - both produce sequences that look unpatterned to a human eye. The difference is in predictability under adversarial conditions.
Math.random() is a PRNG. JavaScript engines implement it using algorithms like xorshift128+ or similar, which are optimized for speed and statistical distribution - not security. The output of Math.random() is deterministic given the internal state of the generator. An attacker who observes enough outputs can reconstruct that state and predict all future values.
OWASP's cryptographic failures category, formerly "Sensitive Data Exposure", is A04 in the OWASP Top 10 (2025). Using weak randomness for security-sensitive tokens is explicitly listed as a cryptographic failure.
AI-generated token code often reaches for Math.random() where a cryptographically secure alternative is required. The reason is simple: AI training data contains thousands of examples of Math.random() used for game logic, shuffling, and display - the model generalizes from those examples without understanding the security context.
How AI Tools Generate Insecure Tokens
Ask Bolt, Cursor, or Lovable to "generate a password reset link" or "create a unique invite code" and you'll frequently see patterns like these:
// ❌ BAD - Math.random() for security tokens
function generateResetToken() {
// AI-generated: "creates a unique token for password reset"
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
// ❌ BAD - Math.random() for session IDs
function generateSessionId() {
return Math.random().toString(36).substr(2, 9);
}
// ❌ BAD - Math.random() for invite codes
function generateInviteCode() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 8; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
// ❌ BAD - Math.random() for CSRF tokens
const csrfToken = Math.random().toString(16).slice(2);
Each of these is predictable. The generator is seeded from system entropy, but its algorithm was built for speed, not secrecy, and in V8 (the JavaScript engine used by Node.js and Chrome) its internal state can be recovered from observed outputs. V8's own team says xorshift128+ is not cryptographically secure.
Understanding the Attack
Public tools such as v8_rand_buster take a list of leaked values like Math.floor(Math.random() * N), solve for V8's xorshift128+ state, and then generate the values that come next. Once the internal state is known, future values are predictable.
For a password reset flow, this means: if an attacker can trigger multiple password reset requests and observe the token patterns in reset emails (or URLs), they may be able to predict the token generated for a victim's account and take it over - without ever having access to the email.
The Secure Alternatives
// ✅ GOOD - crypto.randomUUID() for unique IDs and tokens
import { randomUUID } from 'crypto'; // Node.js
function generateResetToken() {
// Version 4 UUID: 122 cryptographically secure random bits
return randomUUID(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
// ✅ GOOD - crypto.randomBytes() for custom-length tokens (Node.js)
import { randomBytes } from 'crypto';
function generateSessionId() {
// 32 bytes = 256 bits of secure randomness, hex-encoded
return randomBytes(32).toString('hex');
}
// ✅ GOOD - crypto.getRandomValues() for browser-side code
function generateInviteCode() {
// 32 characters, so b % 32 maps all 256 byte values evenly (no modulo bias)
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const array = new Uint8Array(8);
crypto.getRandomValues(array); // Browser Web Crypto API
return Array.from(array)
.map(b => chars[b % chars.length])
.join('');
}
// ✅ GOOD - For Supabase/Next.js: use built-in auth token generation
// Never generate your own reset tokens - use auth provider's built-in flow
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/reset-password`
});
The last example is important: for password reset flows in particular, the safest approach is to not generate tokens yourself at all. Supabase, NextAuth, Auth.js, and Clerk all handle token generation with proper cryptographic randomness internally. Use their APIs instead of building your own.
When Math.random() Is Fine (and When It Isn't)
| Use case | Math.random() safe? | Recommendation |
|---|---|---|
| Random UI animations | Yes | Fine - no security impact |
| Shuffling a display list | Yes | Fine - not security-sensitive |
| Game mechanics, dice rolls | Yes | Fine unless real money is involved |
| A/B test assignment | Usually | Fine unless you need unpredictability |
| Password reset tokens | No | Use crypto.randomBytes(32) |
| Session IDs | No | Use crypto.randomUUID() |
| CSRF tokens | No | Use crypto.randomBytes(32) |
| API keys / secrets | No | Use crypto.randomBytes(32) |
| Invite / referral codes | No | Use crypto.getRandomValues() |
| Lottery / gambling | No | Use CSPRNG - legal requirement in many jurisdictions |
How to Find Insecure Randomness in Your Codebase
Search your codebase for Math.random() and then check the context of each occurrence. If the output is used to generate a token, ID, key, code, or anything that needs to be unguessable, it's a vulnerability. Common patterns to flag: Math.random().toString(36), Math.random() * 1000000 | 0 for numeric IDs, and Math.floor(Math.random() * chars.length) in token-building loops.
Tools like VibeDoctor's Vibe Check automatically flag Math.random() on lines that build a token, session ID, secret, salt, nonce, OTP, or similar value (and Python's random module next to such lines), with specific file paths and line numbers. When the keyword is not on the same line, as with return Math.random().toString(36) inside generateResetToken(), the call can slip past, so still do the search above. Free to sign up.
The crypto module is built into Node.js (no npm install required) and the Web Crypto API (crypto.getRandomValues()) is available in all modern browsers and in Next.js edge runtime. There's no dependency cost to switching - just a one-line change per occurrence.
FAQ
How many bits of entropy does Math.random() provide?
Math.random() returns a 64-bit float, but V8's implementation uses a 128-bit internal state seeded from system entropy at startup. In current V8 each call yields 53 random bits - fewer than the 128 bits you get from crypto.randomBytes(16). More critically, the outputs are correlated, and tools exist that recover the full state from a short run of observed values.
Is crypto.randomUUID() available in the browser?
Yes - crypto.randomUUID() is part of the Web Crypto API and available in all modern browsers (Chrome 92+, Firefox 95+, Safari 15.4+) on pages served over HTTPS (or localhost). It's also available in Node.js 14.17+ and in Next.js edge runtime. No polyfill or package needed.
Should I use uuid npm package or crypto.randomUUID()?
For v4 UUIDs (random), crypto.randomUUID() is the better choice - it's built-in, has no installation overhead, and uses the platform's CSPRNG directly. The uuid package remains useful if you need specific UUID versions (v1, v5) or older Node.js/browser support.
Does Supabase generate secure tokens for auth flows?
Yes. Supabase's auth.resetPasswordForEmail(), auth.signInWithOtp(), and session token generation all use server-side cryptographically secure randomness. You should use these methods rather than building your own token flows with client-side random generation.
What about nanoid - is it secure?
nanoid uses the Web Crypto API (crypto.getRandomValues()) in both the browser and Node.js in current versions - it is cryptographically secure. It's a good choice when you need short, URL-friendly IDs. Just make sure you're using the standard import, not the non-secure variant (nanoid/non-secure, which uses Math.random()).