alias.io

I'm Elbert, a Dutch expatriate programmer in Australia. I use and create open-source software.

I'm currently employed as Technical Lead at Tundra Interactive, a digital agency in Melbourne.

Contact

honeypot@alias.io PGP Key

How to store passwords safely with PHP and MySQL

Published on January 31, 2010, updated on April 25, 2013

First, let me tell you how not to store passwords and why.

Do not store password as plain text

This should be obvious. If someone gains access to your database then all user accounts are compromised. And not only that, people tend to use the same password on different sites so those accounts will be compromised as well. Your site doesn't even need to be hacked; a system administrator on a shared server could easily browse your database.

Do not try to invent your own password security

Chances are that you're no security expert. You're better off using a solution that has been proven to work instead of coming up with something yourself.

Do not ‘encrypt’ passwords

Encryption may seem like a good idea but the process is reversible. Anyone with access to your code would have no trouble transforming the passwords back to their originals. Security through obscurity is not sufficient!

Do not use MD5

Storing password hashes is a step in the right direction. Cryptographic hashing functions like MD5 are irreversible which makes it difficult to figure out the original password. To validate a hashed password, simply hash the password again when a user logs in and compare the hashes.

$password = 'swordfish';

$hash = md5($password); // Value: 15b29ffdce66e10527a65bc6d71ad94d

Note that this makes it impossible to retrieve the original password from the database. If a user forgets his password, simply generate a new one.

So why not MD5? It is quite easy to make a list of millions of hashed passwords (a rainbow table) and compare the hashes to find the original passwords (the same goes for other hashing functions like SHA-1).

MD5 is also prone to brute forcing (trying out all combinations with an automated script) particularly because of collisions. This means that different passwords can have the same hash, making it even easier to find one that works.

MD5 collision demo: mscs.dal.ca/~selinger/md5collision.

Do not use a single site-wide salt

A salt is a string that is hashed together with a password so that most rainbow tables (or dictionary attacks) won't work.

$password = 'swordfish';
$salt = 'something random';

$hash = md5($salt . $password); // Value: db4968a3db5f6ed2f60073c747bb4fb5

This is better than using just MD5 but someone with access to your code could find the salt a generate a new rainbow table.

What you should do

  • Use a cryptographically strong hashing function like bcrypt (see PHP's crypt() function).
  • Use a random salt for each password.
  • Use a slow hashing algorithm to make brute force attacks practically impossible.
  • For bonus points, regenerate the hash every time a users logs in.
$username = 'Admin';
$password = 'gf45_gdf#4hg';

// A higher "cost" is more secure but consumes more processing power
$cost = 10;

// Create a random salt
$salt = strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');

// Prefix information about the hash so PHP knows how to verify it later.
// "$2a$" Means we're using the Blowfish algorithm. The following two digits are the cost parameter.
$salt = sprintf("$2a$%02d$", $cost) . $salt;

// Value:
// $2a$10$eImiTXuWVxfM37uY4JANjQ==

// Hash the password with the salt
$hash = crypt($password, $salt);

// Value:
// $2a$10$eImiTXuWVxfM37uY4JANjOL.oTxqp7WylW7FCzx2Lc7VLmdJIddZq

In the above example we turned a reasonably strong password into a hash that we can safely store in a database. The next time the user logs in we can validate the password as follows:

$username = 'Admin';
$password = 'gf45_gdf#4hg';

// For brevity, code to establish a database connection has been left out

$sth = $dbh->prepare('
  SELECT
    hash
  FROM users
  WHERE
    username = :username
  LIMIT 1
  ');

$sth->bindParam(':username', $username);

$sth->execute();

$user = $sth->fetch(PDO::FETCH_OBJ);

// Hashing the password with its hash as the salt returns the same hash
if ( hash_equals($user->hash, crypt($password, $user->hash)) ) {
  // Ok!
}

A few additional tips to prevent user accounts from being hacked:

  • Limit the number of failed login attempts.
  • Require strong passwords.
  • Do not limit passwords to a certain length (remember, you're only storing a hash so length doesn't matter).
  • Allow special characters in passwords, there is no reason not to.

That's it, happy coding!