0x55aa
← Back to Blog

Insecure Randomness: Why Math.random() Is a Security Disaster 🎲

5 min read

Insecure Randomness: Why Math.random() Is a Security Disaster 🎲

Picture this: you're building a password reset feature at 11 PM the night before launch. You need a unique token. You type:

const token = Math.random().toString(36).substring(2);

It looks random. It feels random. You ship it.

Somewhere out there, a hacker runs a script for 30 seconds and resets your user's password. Not because your code was broken — it worked exactly as designed. The problem is that "random" and "cryptographically secure random" are completely different things, and mixing them up is one of the most quietly devastating mistakes in software development.

Why Math.random() Is Not Random Enough 🤔

Math.random() gives you a pseudorandom number. Meaning: it looks random to a human, but it's generated by a deterministic algorithm called an PRNG (Pseudorandom Number Generator).

In V8 (Node.js's engine), Math.random() uses an algorithm called xorshift128+. It's fast and statistically decent for simulations and games. But it has one fatal flaw for security: if an attacker sees enough output, they can predict future values.

Here's the real-world attack:

  1. An attacker generates password reset tokens for a few throwaway accounts on your site
  2. They observe the token values — which are Math.random() outputs in disguise
  3. They feed those values into a reverse-engineering script
  4. They recover the internal PRNG state
  5. They predict the next tokens your server will generate — including the one just emailed to your victim

This isn't theoretical. Security researchers have published tools that crack V8's random number generator in milliseconds given just a handful of observed values.

The Code That'll Get You Hacked 💀

Here's the kind of thing that shows up in production codebases:

// 🚨 INSECURE - Don't do this!
function generatePasswordResetToken() {
  return Math.random().toString(36).substring(2) +
         Math.random().toString(36).substring(2);
}

function generateSessionId() {
  return 'sess_' + Math.random().toString(36).substring(2, 15);
}

function generateApiKey() {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let key = '';
  for (let i = 0; i < 32; i++) {
    key += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return key;
}

Every one of these is broken. The tokens look long and complex, but they're all derived from a predictable internal state. Concatenating two Math.random() calls doesn't help — it just gives attackers more data to work with.

The Right Way: Cryptographically Secure Randomness ✅

The fix is to use a CSPRNG (Cryptographically Secure Pseudorandom Number Generator). These are designed specifically so that even if you see a million outputs, you cannot predict the next one.

In Node.js, use the built-in crypto module:

const crypto = require('crypto');

// ✅ SECURE - Use these instead!
function generatePasswordResetToken() {
  // 32 bytes = 256 bits of entropy. Good luck predicting that.
  return crypto.randomBytes(32).toString('hex');
}

function generateSessionId() {
  return 'sess_' + crypto.randomBytes(16).toString('base64url');
}

function generateApiKey() {
  // UUID v4 using crypto - also secure
  return crypto.randomUUID();
}

// Need a random number in a range? Use randomInt
function rollDiceSecurely(sides) {
  return crypto.randomInt(1, sides + 1);
}

In a browser (or modern Node.js with the Web Crypto API):

// ✅ Browser-safe CSPRNG
function generateBrowserToken() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array); // window.crypto, not Math
  return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}

The difference? crypto.randomBytes() draws entropy from the operating system's own entropy pool (/dev/urandom on Linux), which is seeded from hardware events — keyboard timing, network noise, disk I/O jitter. It's computationally infeasible to predict.

Real Things That Went Wrong 🔥

This isn't just a theoretical concern:

  • Meteor.js (2013): Token generation used a weak PRNG. Attackers could predict password reset links. This got a CVE and a lot of angry developers.
  • Steam (2019): A researcher found trade confirmation codes were generated with predictable entropy, enabling token prediction.
  • Countless internal apps: If you've ever audited a legacy Node.js or PHP app, you've seen rand() or Math.random() generating tokens. It's everywhere.

The pattern is always the same: developer picks the most convenient random function, not the most secure one.

The Quick Rule to Remember 🧠

Ask yourself: "Does this value need to be secret or unguessable?"

If yes → use your crypto library. If no (animations, A/B test splits, game dice rolls) → Math.random() is fine.

Security-sensitive contexts include: password reset tokens, session IDs, API keys, CSRF tokens, email verification codes, OAuth state parameters, magic login links, and anything a user authenticates with.

Not security-sensitive: shuffling a playlist, picking a random background color, generating test data, dice rolls in a game.

When in doubt: crypto costs almost nothing in performance and prevents everything in exploits.

One More Gotcha: Token Length Matters Too 📏

Even with crypto.randomBytes(), if you generate only 4 bytes (32 bits), an attacker can brute-force all ~4 billion possibilities in seconds. Aim for at least 16 bytes (128 bits) of entropy for tokens. 32 bytes (256 bits) for anything long-lived like API keys.

// ✅ 32 bytes = 64 hex chars = 256 bits of entropy
const token = crypto.randomBytes(32).toString('hex');

That's 2^256 possible values. Even if you could check a trillion tokens per second, you'd need longer than the age of the universe to brute-force it. Good enough.


The gap between "random" and "cryptographically secure random" is invisible in code but enormous in security impact. Math.random() is a loaded gun in a security-sensitive context — and the worst part is it never throws an error, never warns you, and looks completely fine in code review.

Next time you need a token, reach for crypto.randomBytes(). Your users will never know the difference. Attackers will notice immediately.


Found a Math.random() token lurking in your codebase? Drop a comment or share this with the dev who needs to see it. Follow me on GitHub for more security deep-dives.