PHP: How do you generate a random string?

You need a random string again and again. For a file name, for a cache key or for a token. Depending on what you use it for, though, quite different things matter.

Ask yourself the important question up front: is anyone allowed to guess the string? If it is only a unique file name, the answer does not matter. If it is a password reset link or a session token, the security of your application depends on it.

The simple case

The shortest route to a secure random string goes through random_bytes and bin2hex:

<?php
  echo bin2hex(random_bytes(16));
?>

Output:

71649e32bddbae7ad9149822227aa85e

random_bytes() returns the requested number of bytes from the operating system’s cryptographically secure source. bin2hex() converts those into readable hexadecimal characters. 16 bytes become 32 characters, because every byte needs two of them.

The result consists only of 0-9 and a-f. For most purposes that is perfectly fine and you do not need to read any further.

Your own alphabet

If you want to decide which characters may occur, draw the characters individually with random_int:

<?php
function randomString(int $length, string $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'): string {
  $max = strlen($alphabet) - 1;
  $out = '';

  for ($i = 0; $i < $length; $i++) {
    $out .= $alphabet[random_int(0, $max)];
  }

  return $out;
}

echo randomString(12);   // e.g. 7t3jyvhkma33
?>

random_int() is the secure variant of rand() and returns a number in the given range — without the skew we are about to come to.

Why not rand() or mt_rand()?

Because their output can be reproduced. Neither function generates real random numbers; they generate a sequence that depends entirely on a starting value (the seed). Set the same seed and you get the same sequence:

1st run with seed 42: 637542, 126067, 286876, 846414, 890026
2nd run with seed 42: 637542, 126067, 286876, 846414, 890026
identical: true

For an attacker that means: whoever knows the seed, or can derive it from observed values, knows all the following values too. For test data that same property is actually useful, because it lets you reproduce a bug. For tokens it is fatal.

The trap: modulo bias

The following line is very common for mapping a random byte onto an alphabet:

<?php
  // Careful, this variant is skewed!
  $char = $alphabet[ord(random_bytes(1)) % strlen($alphabet)];
?>

The problem has nothing to do with the random source and everything to do with the remainder of the division. A byte has 256 possible values. With a 10 character alphabet, 256 / 10 = 25 remainder 6. So the first six characters of the alphabet get one chance more than the remaining four.

That is not grey theory, it can be measured. 600,000 draws, alphabet 0123456789:

  0: 10.229 %
  1: 10.119 %
  2: 10.167 %
  3: 10.159 %
  4: 10.096 %
  5: 10.126 %
  6:  9.810 %
  7:  9.778 %
  8:  9.755 %
  9:  9.760 %

The step sits exactly between 5 and 6, precisely where the arithmetic predicts it. For comparison, the same number of draws with random_int(0, 9):

  0: 10.085 %
  1:  9.997 %
  ...
  8: 10.008 %
  9:  9.986 %

There is no such block here, the deviations scatter randomly. 9.76 % instead of 10 % sounds like very little. With a token it means an attacker can order their guesses sensibly. random_int() takes that worry off your hands, because internally it re-draws until the value falls into the desired range without skew.

uniqid() is not random

A common misunderstanding. uniqid() does not generate random values, it formats the current time. Three calls in a row:

6a91e83188307
6a91e8318830b
6a91e8318830c

The values differ only in their last characters and count upwards. As an identifier within one request that is usable, as a token it is not.

It looks similar with str_shuffle. The function shuffles the characters you pass in, so every character occurs exactly once afterwards. For a random string drawn from an alphabet it is therefore the wrong choice, quite apart from the question of the random source.

Summary

  • For tokens, links and keys: random_bytes() or random_int().
  • For everything else bin2hex(random_bytes(16)) does just as well.
  • rand() and mt_rand() are reproducible and unsuitable for security purposes.
  • Never map bytes onto an alphabet with %, it skews the distribution.
  • uniqid() is a clock and str_shuffle() is a permutation. Neither is a random generator.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).