dummyipsumRandom strings end up everywhere: API keys, password reset links, invite codes, session IDs, file...
Random strings end up everywhere: API keys, password reset links, invite codes, session IDs, file names. Most of the time the code that generates them is three lines long and nobody looks at it again. Here are five mistakes I keep running into, with a fix for each in the languages most of us use.
// Don't do this for anything secret
const token = Math.random().toString(36).slice(2);
Math.random() is fast and fine for shuffling a playlist, but it is not a cryptographically secure generator. MDN says it plainly: do not use it for anything related to security. V8 uses xorshift128+, and given a handful of outputs, its internal state can be recovered and the next values predicted.
The one-liner is also short and uneven: in a quick run of 100,000 calls the result was anywhere from 8 to 14 characters long, usually 11.
Use the secure generator your platform already ships:
// Browser and Node.js 19+
crypto.getRandomValues(new Uint8Array(32));
// Node.js
require('crypto').randomBytes(32).toString('hex');
import secrets
secrets.token_hex(32) # 64 hex characters
bin2hex(random_bytes(32)); // 64 hex characters
openssl rand -hex 32
So you switched to a secure source and want letters and numbers only:
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const bytes = crypto.getRandomValues(new Uint8Array(32));
const token = Array.from(bytes, (b) => alphabet[b % 62]).join('');
It looks random, but it is not uniform. A byte has 256 values, and 256 is not divisible by 62: 256 = 4 × 62 + 8. The first 8 characters of the alphabet (A to H) get 5 chances out of 256, all the others get 4. That makes them 25% more likely. I ran it over 6.5 million bytes to be sure: A came up 128,293 times, z 102,634 times, a ratio of 1.25.
For a 32-character token the loss of entropy is small, but it is free to avoid. Throw away the values that fall into the uneven tail (rejection sampling):
function randomString(length, alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
const limit = 2 ** 32 - (2 ** 32 % alphabet.length);
let value = '';
while (value.length < length) {
const [n] = crypto.getRandomValues(new Uint32Array(1));
if (n < limit) value += alphabet[n % alphabet.length];
}
return value;
}
In Python you don't have to think about it, secrets.choice() is already uniform:
import secrets, string
alphabet = string.ascii_letters + string.digits
''.join(secrets.choice(alphabet) for _ in range(32))
"16 characters sounds long enough" is how weak tokens happen. The number that matters is bits of randomness:
bits = length × log2(alphabet size)
For letters and numbers (62 characters, about 5.95 bits each):
| Length | Bits |
|---|---|
| 8 | 47.6 |
| 16 | 95.3 |
| 22 | 131 |
| 32 | 190.5 |
| 43 | 256 |
Hex carries exactly 4 bits per character, so 64 hex characters are 256 bits.
OWASP asks for at least 64 bits of entropy for session IDs, and 128 bits is a common target for anything long-lived like API keys or reset tokens. For a JWT HS256 secret, match the hash size: 256 bits, which is 43 alphanumeric or 64 hex characters.
require('crypto').randomBytes(32).toString('base64');
// 'MNRSbqb16Oi8sGwLu+bW2yCcsjhUoYPhNemI1Sl3m/w='
Standard Base64 uses +, / and =. Put that token in a query string and + turns into a space when it is decoded, / collides with paths and = needs escaping. The reset link works in your tests and fails for the one user whose token happened to contain a +.
Use the URL-safe variant, which swaps in - and _ and drops the padding:
require('crypto').randomBytes(32).toString('base64url'); // 43 characters
secrets.token_urlsafe(32) # 43 characters
rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
If you are curious why a space becomes %20 in one place and + in another, I wrote that up separately: https://www.dummyipsum.com/blog/url-encoding-why-spaces-become-20
UUIDs are great IDs, and that is the problem: they look random enough to be used as secrets too.
Use UUIDs for primary keys and correlation IDs. For anything that grants access (a reset link, an invite, an API key), generate a token from a secure source as in the sections above.
Invite and voucher codes get read off paper and typed on phones. Leave out the characters people confuse: 0 and O, 1, l and I. Use uppercase letters and digits only, and add a few characters to make up for the smaller alphabet. Uppercase and digits without 0, O, 1 and I leave 32 characters, so ten of them give you 50 bits, plenty for a code that is rate-limited on the server.
When I just need a token for a .env file or test data and don't want to open a REPL, I use the generator on my site. It runs in the browser with crypto.getRandomValues, does the rejection sampling from section 2, and handles hex, look-alike-free codes, prefixes and up to 1,000 strings at once: https://www.dummyipsum.com/random-string-generator
What is the worst token generation code you have seen in production?