🔀 CSPRNG vs Math.random(): Why Your Token Generator Is Broken in 2026
Math.random() is not one.
One line of code — Math.random() — has quietly handed attackers valid session tokens, guessable password-reset links, and predictable API keys in shipped production apps. The fix is not more entropy or a longer string: it is using a CSPRNG instead of a general-purpose random function. A CSPRNG is the only safe source of randomness for any secret value in 2026; ordinary generators like Math.random() are predictable by design and must never produce a password, token, or key.
The trap is that the insecure version looks fine. Its output is a jumble of characters that passes every casual eyeball test. The weakness is invisible until someone who understands the underlying algorithm decides to exploit it — and by then the tokens are already live.
What Makes a Random Number "Secure"?
There are two very different jobs that go by the name "random." A statistical RNG only needs its output to be evenly spread and free of obvious patterns — perfect for shuffling a playlist, jittering an animation, or picking a sample. A cryptographic RNG has a far harder requirement: even an attacker who has seen many outputs must be unable to predict the next one or recover the ones before it.
That second property is called unpredictability, and it is the entire game. A CSPRNG achieves it two ways: it is seeded from the operating system's entropy pool (hardware noise, interrupt timings, and other unpredictable sources), and its algorithm is built so that observing the output reveals nothing usable about the internal state. General-purpose generators deliberately trade this away for raw speed.
Why Math.random() Fails
JavaScript's Math.random() is typically implemented with an algorithm from the xorshift family. It is fast and statistically smooth, but it keeps a small internal state that is fully determined by past outputs. Researchers have repeatedly shown that an attacker who collects a modest run of consecutive values can solve for that state and then reproduce every past and future output of the generator. Seeded from the clock and never reseeded with real entropy, it offers no unpredictability at all.
This is not a JavaScript-only problem. The same flaw lives in C's rand(), Java's java.util.Random, Python's top-level random module, and PHP's mt_rand(). All are Mersenne-Twister or linear-congruential generators built for simulations, not secrets. The OWASP community catalogues this exact mistake:
General RNG vs CSPRNG at a Glance
| Property | General RNG (Math.random) | CSPRNG |
|---|---|---|
| Design goal | Speed & even distribution | Unpredictability |
| Seed source | Clock / fixed seed | OS entropy pool |
| State recovery | Feasible from few outputs | Computationally infeasible |
| Safe for secrets? | No | Yes |
| Typical use | Games, sampling, UI | Passwords, tokens, keys, salts |
The Right Tool in Every Language
The good news: every major runtime ships a CSPRNG in its standard library, and it is usually a one-liner. You almost never need a third-party package. Reach for these instead of the general-purpose functions:
| Language | Secure API | Avoid |
|---|---|---|
| Browser JS | crypto.getRandomValues() | Math.random() |
| Node.js | crypto.randomBytes(), randomUUID() | Math.random() |
| Python | secrets module | random module |
| Go | crypto/rand | math/rand |
| Java | java.security.SecureRandom | java.util.Random |
| Rust | rand::rngs::OsRng | seedable StdRng for secrets |
| Linux/CLI | /dev/urandom | timestamp / PID seeds |
Python 3 makes the intent explicit: its secrets module was added precisely so developers stop reaching for random. Its documentation states plainly that it should be used "for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets."
What a Predictable Token Actually Costs
The abstract risk becomes concrete the moment a weak generator touches an authentication flow. Picture a password-reset feature that builds its reset link from Math.random(). A user clicks "forgot password," the server generates a token, emails a link, and trusts whoever presents that token. Because the generator's state is recoverable, an attacker who triggers a few resets against accounts they control can solve for the internal state, then compute the reset token issued to someone else's account — and take it over without ever knowing the password.
The same failure pattern recurs across features that all quietly depend on unpredictability: session identifiers that can be guessed to hijack a logged-in user, "unguessable" share links that turn out to be enumerable, coupon or gift-card codes minted in sequence, and API keys whose neighbours can be derived once one is known. None of these look broken in testing, because the output still looks random to a human. They break only against an adversary who models the algorithm — which is precisely the adversary security is supposed to stop.
How Much Randomness Is Enough?
Using a CSPRNG solves quality; you still have to supply enough quantity. Randomness is measured in bits of entropy, and each additional bit doubles the number of guesses an attacker must make. The widely accepted floor for a value that must resist brute force is 128 bits, which corresponds to 16 random bytes drawn from a CSPRNG.
- 16 bytes (128 bits) — session tokens, password-reset codes, API keys. The standard target; encodes to a 22-character base64url or 32-character hex string.
- 32 bytes (256 bits) — long-lived or high-value secrets, signing keys, and anything you want to survive decades of hardware progress.
- Salts — 16 bytes is ample; they need uniqueness, not secrecy.
Two rules keep the entropy you generate: never truncate a token to make it "prettier," and never post-process CSPRNG output through a weak function that could reintroduce bias. NIST's guidance on the seeding side is blunt — Special Publication 800-90A specifies that a deterministic random bit generator's strength is capped by the entropy of its seed, which is exactly why drawing from the OS pool, rather than a clock, is non-negotiable.
A Safe Token-Generation Checklist
- Pick the CSPRNG for your language from the table above — never
Math.random()or a barerandommodule. - Request at least 16 bytes (128 bits) of entropy; use 32 bytes for anything long-lived.
- Encode with base64url or hex — encoding changes length, not entropy.
- Compare secrets with a constant-time function to avoid timing side channels.
- Store the resulting secret in a real vault or password manager, not in source code or a plaintext config.
Where This Leaves You
If you write code, the rule reduces to a single habit: whenever a value must be unguessable, reach for the cryptographic API, never the convenient one. The two lines look almost identical, cost the same, and run at effectively the same speed for the tiny amounts of randomness a token needs — but only one of them is safe.
And if you are simply generating a strong password or key for your own use, you should not have to think about any of this. Our Secure Key Generator runs entirely in your browser on crypto.getRandomValues(), so every value is drawn from a genuine CSPRNG and nothing is transmitted anywhere. Once you have generated a strong, unique secret, keep it somewhere it cannot leak: store it in a reputable manager such as NordPass rather than pasting it into a note or a config file. Good randomness and safe storage are the two halves of the same job — get both right and a leaked token stops being a catastrophe.
crypto.getRandomValues(), the secrets module, crypto/rand, or SecureRandom; give every secret at least 128 bits; and never let Math.random() anywhere near a password, token, or key.