
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.

Password Generator: Why Math.random() Passwords Get Cracked (and What to Use Instead) Two years ago I shipped a password reset flow where the temporary password came from Math.random().toString(36).slice(2, 14).
Summary
Twelve characters, mixed letters and digits. It looked random. Our pen tester asked for two consecutive reset emails, pasted both tokens into a script, and reconstructed the generator's internal state in under a minute.
Every "random" password our server could produce was now a short list for him. That day taught me the difference between unpredictable-looking and unpredictable. I have rewritten that generator three times since, for three different employers, and the bug keeps coming back in code review because Math.random() is one line long and looks completely innocent.
Here is how I generate passwords now, in Python, in Node, and when I am too lazy to write code at all. Why Math.random() fails at this the technique is well documented and the tools to do it are public. The MDN page for Math.random() says it outright: it should not be used for anything security-related.
The fix is always the same idea: read randomness from the operating system's CSPRNG instead of a PRNG with recoverable state. Method 1: Python's secrets module (Python 3.11.1) Python makes this almost boring. The secrets module was added exactly for this job, and secrets.choice() picks a character uniformly. # Python 3.11.1 import math import secrets import string ALPHABET = string.ascii_letters + string.digits # 62 symbols def gen_password(length: int = 16) -> str: return "".join(secrets.choice(ALPHABET) for _ in range(length)) for _ in range(3): print(gen_password(16)) print("entropy:", round(16 * math.log2(len(ALPHABET)), 1), "bits") Output from my machine: p65Q27UkKzPhpNwe qUPeJyfHVhF5333I Br13QDK4tsv2zeYl entropy: 95.3 bits Ninety-five bits is well beyond brute-force reach.
The secrets documentation recommends the same pattern for tokens, reset links, and anything else an attacker might guess. Method 2: Node.js crypto.randomInt (Node 22.13.1) In Node, reach for node:crypto. I use crypto.randomInt() per character.
It performs rejection sampling internally, so every index is equally likely, which matters more than most people think (see pitfall 2). Output: qC3nGqZj5HZOwvkw PCh5EqjcXuVvUokb 9eLoGcWlrzZX3nKB In the browser the equivalent is crypto.getRandomValues(), which fills a typed array from the same class of OS entropy. Never reach for Math.random() as a fallback there either.
Method 3: when I just need one password, right now Half the time I am not building a flow. I am setting up a database user or rotating an API credential, and I want one strong password in the next five seconds. Writing a script for that is overhead, and pasting something from a code snippet on the internet is how the modulo-bias bug from pitfall 1 survives into 2026.
This is where I open a free online password generator, set length and character sets, and copy the result. This browser-based tool generates everything locally in JavaScript, so the password never leaves my machine, which is the whole point of a password generator in the first place. If you are auditing your own habits, the password generator is also a fast way to compare what a real 20-character password looks like against whatever your old script produced.
Three pitfalls that quietly weaken "random" passwords 1. Modulo bias when mapping bytes to characters. A common shortcut is charset[randomBytes(1)[0] % charset.length].
With 62 characters, 256 % 62 = 8, so the first 8 symbols get hit 5 times per 256 bytes while the rest get 4. I measured it with 620,000 single bytes in Python 3.11.1: index 0 landed 1.957% of the time versus 1.613% expected. That is roughly 21% more likely than its neighbors, on every single character of every password. secrets.choice() and crypto.randomInt() both avoid this; hand-rolled % does not.
2. Slicing Base64 strings. crypto.randomBytes(12).toString("base64") gives 16 characters of 96-bit entropy, which sounds great until you look at it: +KQcdtawbjaxhglg <- full string +KQcdtawbj <- slice(0, 10): about 60 bits left Cutting to 10 characters throws away a third of the entropy you paid for. The full string also carries + and /, which break URLs and some CSV imports, and a 12-byte input always ends in exactly one = pad.
If someone pastes os.urandom(12) output around, they probably shipped padding and bias together. 3. Requiring "complexity rules" on a strong generator.
Forcing a digit, a symbol, and an uppercase letter into a 12-character password shrinks the effective search space and trains users to predict where the symbol sits (usually the end). A uniform 16-character alphanumeric password at 95.3 bits beats a rule-mangled 12-character one at roughly 71 bits. Length beats punctuation.
Summary Method Entropy (16 chars) Best for Python secrets.choice() ~95 bits Server-side token generation Node crypto.randomInt() Node services, unbiased per-char picks Recoverable state Nothing. Stop. The pattern is simple: OS entropy in, uniform selection, no truncation games.
Everything else is decoration. More free browser-based utilities at utilvy.com.
KazaSec's take
Incidents like this rarely start with the headline event itself, they usually trace back to an exposed remote-access endpoint, an unpatched perimeter system, or a credential phished weeks earlier. The organizations that recover fastest are the ones that tested their defenses and their incident response plan before they needed them.
Coverage details
We've archived 892 other articles touching the same topic (backend, javascript, security) , see the full security news archive.
Relevant from KazaSec
More coverage on this topic
We help organizations find and fix the gaps before they make headlines.