How to store passwords safely with PHP and MySQL

Posted on January 31, 2010

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 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.

1
2
3
4
5
<?php
$password = 'swordfish';
 
$hash = md5($password); // Value: 15b29ffdce66e10527a65bc6d71ad94d
?>

Note that this makes it impossible to retrieve a 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) 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.

1
2
3
4
5
6
<?php
$password = 'swordfish';
$salt = 'something random';
 
$hash = md5($salt . $password); // Value: db4968a3db5f6ed2f60073c747bb4fb5
?>

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

What you should do

  • Use a cryptographically strong hashing function like SHA-1 or even SHA-256 (see PHP’s hash() function).
  • Use a long and random salt for each password.
  • Use a slow hashing algorithm to make brute force attacks near impossible.
  • Regenerate the hash every time a users logs in.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
$username = 'Admin';
$password = 'gf45_gdf#4hg';
 
// Create a 256 bit (64 characters) long random salt
// Let's add 'something random' and the username
// to the salt as well for added security
$salt = hash('sha256', uniqid(mt_rand(), true) . 'something random' . strtolower($username));
 
// Prefix the password with the salt
$hash = $salt . $password;
 
// Hash the salted password a bunch of times
for ( $i = 0; $i < 100000; $i ++ )
{
    $hash = hash('sha256', $hash);
}
 
// Prefix the hash with the salt so we can find it back later
$hash = $salt . $hash;
 
/* Value:
 * e31f453ab964ec17e1e68faacbb64f05bccceb179858b4c482c1b182ff1e440e
 * f1e10feb5b86c6d367e4eb8f90f2cde5648a7db3df8526878f20a77eed00c703
 */
 
?>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
$username = 'Admin';
$password = 'gf45_gdf#4hg';
 
$sql = '
    SELECT
        `hash`
    FROM `users`
        WHERE `username` = "' . mysql_real_escape_string($username) . '"
    LIMIT 1
    ;';
 
$r = mysql_fetch_assoc(mysql_query($sql));
 
// The first 64 characters of the hash is the salt
$salt = substr($r['hash'], 0, 64);
 
$hash = $salt . $password;
 
// Hash the password as we did before
for ( $i = 0; $i < 100000; $i ++ )
{
    $hash = hash('sha256', $hash);
}
 
$hash = $salt . $hash;
 
if ( $hash == $r['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!

Scroll to top

Comments (11)

  • I like your article !

    However you presume usernames are fixed and TheName is another user as thename > strtolower($username)

    Posted by Ramon Fincken on January 31, 2010 Reply

  • Thanks Ramon! Actually it's the opposite, strtolower() makes sure the login to is case insensitive (in the SQL query case doesn't matter).

    Posted by ElbertF on January 31, 2010 Reply

  • That's a big salt. I was thinking it would be better to have a secret component in the salt but upon further thought it wouldn't help.
    Thanks for the article ^^ Death to plaintext password storage!

    Posted by Tim Cinel on January 31, 2010 Reply

  • I don't fully understand what the random salt is for. A single site-wide salt is bad because “someone with access to your code can find the salt”, but I don't see how prefixing the hashed string with a random salt makes any difference.

    If I have access to the database, I could just look at the first 64 characters and create a new rainbow table. The only real difference is that it will take more time to “decode” the password of ALL users, but when you're looking at a single password, this method doesn't seem more secure.

    Also, I'm not a security expert :) , but is it really better to hash something multiple times?

    Posted by Tijn on February 11, 2010 Reply

  • Yes that's true. If you're simply after the admin password and know the full hash and algorithm the random salt won't make a difference. The only thing that can help you is a strong password (you could store the hash on a different server).

    Hashing multiple times may increase the chance of collisions slightly but I don't think it matters to much in this case. You'd still have to perform the hash a 100,000 times which slows brute forcing down significantly. If computers get faster you can just hash all the passwords some more.

    Posted by ElbertF on February 11, 2010 Reply

  • Why not use the sleep() function instead of hashing multiple times ?

    Then :
    - you wouldn't increase the chance of collisions
    - you wouldn't increase the server load

    Posted by PA on February 15, 2010 Reply

  • Because a potential hacker would be able to brute-force the password on another machine without using sleep(). You need the algorithm to be slow.

    Posted by ElbertF on February 15, 2010 Reply

  • Doesn't work as written. Need to concatenate the user's input password with the salt again after getting it from the login form . . .

    … . $r = mysql_fetch_assoc(mysql_query($sql));

    // The first 64 characters of the hash is the salt
    $salt = substr($r['hash'], 0, 64);

    $hash = $hash . $password; <—————— INSERT THIS LINE HERE (IN 2ND CODE SECTION)

    // Hash the password as we did before
    for ( $i = 0; $i < 100000; $i ++ )
    {
    $hash = hash('sha256', $hash);
    } … . // ETCETERA

    Posted by Name on February 18, 2010 Reply

  • <———- CORRECTION TO THE ABOVE ————->
    I had a typo in there . . .

    Doesn't work as written. Need to concatenate the user's input password with the salt again after getting it from the login form . . .

    … . $r = mysql_fetch_assoc(mysql_query($sql));

    // The first 64 characters of the hash is the salt
    $salt = substr($r['hash'], 0, 64);

    $hash = $salt . $password; <—————— (TYPO FIXED) INSERT THIS LINE HERE (IN 2ND CODE SECTION)

    // Hash the password as we did before
    for ( $i = 0; $i < 100000; $i ++ )
    {
    $hash = hash('sha256', $hash);
    } … . // ETCETERA

    Posted by Name on February 18, 2010 Reply

  • The above is good for a general rule of thumb, however one must always be ready to be flexible when needed.

    For one thing, not all websites are a vacumn, thus whatever mechanism you may include the OPTION of validating at least initially to a different format. For example, many systems do store MD5 passwords, and it is a simple matter to export them when migrating. So for a special use case[migration of users], you would want to allow the user to sign in using their old password, and then immediately rehash and save it.

    Additionally, you may be working on a web service for other users, in which case there should be an option to export data in a manner which allows people to use that data on their own server. Locking people into your system is rude and obnoxious, there should always be a migration option.

    Lastly, there are indeed times when you will need to allow a site owner to log on as someone else. Customer service is key. In those cases, explaining how much more secure the password system is is useless, you must give them a way to log on. Either store the password in some manner that they can look it up, or provide functionality to allow an admin to “impersonate” a user.

    Posted by garyamort on February 18, 2010 Reply

  • Fixed it, thanks.

    Posted by ElbertF on March 01, 2010 Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

Fork me on GitHub