Math.random() is not a security token generator
By Seaworthy · August 13, 2026
Photo by William Warby on Unsplash
Ask an AI assistant to generate a password reset token, an API key, or a session identifier, and there's a real chance you'll get something built on Math.random():
function generateToken() {
return Math.random().toString(36).slice(2)
}
It looks reasonable. The output looks random. It's a string of letters and numbers, it's different every time you call it, and if you print a hundred of them out you won't spot a pattern by eye. That's exactly the problem: it's random enough to fool a glance, and nowhere near random enough to resist someone actually trying to predict it.
Why "looks random" isn't the bar
Math.random() is a pseudorandom number generator built for things like shuffling a UI animation or picking a random background color, not for anything security-sensitive. Its output is generated from an internal state using a deterministic algorithm, in V8's case an implementation of xorshift128+. Deterministic means that if you can observe enough consecutive outputs, or in some cases even one, you can reconstruct the internal state and predict every value the generator will produce from then on. Researchers have published working state-recovery attacks against V8's Math.random() from as few as a handful of samples.
For a password reset token, that's the whole security model gone. The token doesn't need to be guessed by brute force, it can be computed directly once the generator's state is known, and every token issued from that same process afterward is fully predictable.
Not a bug in the language, a mismatch in the use
Math.random() isn't broken. It does exactly what a general-purpose PRNG is supposed to do: produce statistically well-distributed numbers, fast, with no guarantee of unpredictability against an adversary who's paying attention. The mismatch is entirely about applying a non-cryptographic tool to a job that requires cryptographic randomness, and that mismatch is invisible in code review unless you already know to look for it. generateToken() reads the same whether it's backed by Math.random() or something secure.
The fix is built in and shorter than the broken version
Node has had a cryptographically secure random source in the standard library since version 15, no dependency required:
const crypto = require('crypto')
function generateToken() {
return crypto.randomBytes(32).toString('hex')
}
crypto.randomBytes pulls from the operating system's CSPRNG, the same source used to generate TLS session keys, and is specifically designed to be unpredictable even to an attacker who can observe its output. For a 32-byte token that's 256 bits of entropy, more than enough headroom for a password reset link or an API key. In the browser, crypto.getRandomValues() is the equivalent, also available with no dependency.
If you want a URL-safe token instead of hex, crypto.randomUUID() gets you there in one call and is backed by the same secure source:
const token = crypto.randomUUID()
Why this keeps showing up in AI-generated code
Math.random() is the first thing most training data associates with "generate a random string in JavaScript," because it's genuinely the right answer for the overwhelming majority of non-security uses that string of text appears next to. An assistant pattern-matching on "generate a token" doesn't reliably distinguish that request from "generate a random number for a game," because the surface-level task description is nearly identical. The security requirement lives in what the token is used for, not in the phrasing of the request, and that's exactly the kind of context an autocomplete-shaped suggestion tends to drop.
The fix costs nothing. crypto.randomBytes is the same number of lines as the Math.random() version, ships with Node, and has no learning curve worth mentioning. The only real obstacle is knowing to ask the question in the first place.
Seaworthy flags Math.random() used anywhere in a token, session ID, or password reset code generation path, since it's one of the few checks where the fix is genuinely a one-line swap once the pattern is caught.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.