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 (32)

  • 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

  • Sorry but I thought that it is well documented that just doing this with MD5 -

    $HashedPassword = $md5(md5($Password));

    Increases the chance of collision dramatically?

    So surely when running this -
    for ( $i = 0; $i < 100000; $i ++ )
    {
    $hash = hash('sha256', $hash);
     }

    you are increasing the chance of hash collision?

    Im no expert and im unsure if MD5 and SHA256 work the same but I do know that it is a big NO NO to rehash a hash as it increases chances of collision.

    Posted by Andrew on August 18, 2010 Reply

  • great posts thanks a lot ElbertF ;)

    thanks too for wappalyzer !

    Posted by Theodor on January 06, 2011 Reply

  • I agree with @Andrew - I think that this really provides no extra security, increases chance of collision, and eats up server resources. Instead, I would just implement a “3 strikes and your out” method instead…

    Posted by solo on February 22, 2011 Reply

    • Oh and obviously I’m just talking about hashing 100000 times…

      Posted by solo on February 26, 2011 Reply

    • Hashing SHA-256 over and over does not increase the chance of collisions AFAIK (as is the case with MD5), this is from what I’ve read and tests I’ve done myself. The added security comes from the time it takes to generate the hash, the same way Bcrypt does (you could of course just use Bcrypt but it’s not always available on shared hosts). Eating up server resources is a feature in this case. ;) “Three strikes and your out” is indeed useful but not if someone gets a hold of your database.

      Posted by ElbertF on March 23, 2011 Reply

  • Thank you, very nice post, everything is perfectly clear!

    Wish you write something about how to work with sessions in a way that a potential hacker could not log in even if he has got the DB and the sources both.

    Posted by Kremchik on April 28, 2011 Reply

  • LONG LIVE ROT13!

    lol, I jest.. :)

    Cool post.

    Posted by Dave on August 24, 2011 Reply

  • Your article has helped me alot. Thank you so much.

    Posted by deepz on September 05, 2011 Reply

  • If i put the md5($hash) in a cookie. Its possible to recover the password?

    Just need to make sure.

    Posted by Tiago Roldão on November 17, 2011 Reply

    • I’m afraid storing the password in a cookie, hashed or not, is not safe. The password doesn’t even need to be recovered for an attacker to take over the account if they know the hash.

      Posted by ElbertF on December 04, 2011 Reply

  • Thanks for the concept!

    Posted by Champ Polestico on November 30, 2011 Reply

  • Thank you so much for this article. It’s quite useful.

    Posted by Doaa on December 05, 2011 Reply

  • Hi.. Thank you for the article. Is it code fixed with the code in comment? I’ll try it.

    Once again, thank you.. :D

    Posted by Reyz on December 05, 2011 Reply

  • My code doesn’t work.. :(

    Posted by Reyz on December 06, 2011 Reply

  • Thank you so much. I had one question, in my application I have a “Forgot Password” link, in which I send passwords to the users on their emails. If I stored the passwords hashed, how can I restore the original plain text to be sent?

    Posted by Doaa on December 06, 2011 Reply

    • I know that the hash cannot be reveresed. But what can be done in this case? An ideas?

      Posted by Doaa on December 06, 2011 Reply

      • Generate a new password.

        Posted by Anonymous on December 15, 2011 Reply

  • Notify user with an email which holds a link to create a new password. The link should also hold some encrypted character which can be active only for a certain time period may be 10 mins.

    Posted by JangoVimal on January 11, 2012 Reply

  • Thanks very much! This seems to work quite well, but as somewhat of a newbie, it appears that the passwords are not unique, wherein the uniqueness comes from the username. The password uniqueness comes from the hashed match to the entered password, not to the password, as separate entries of the same password will create unique hashes for each password entry. With the number of iterations required to have a collision, is this deemed acceptable, or am I possibly doing something wrong, and each entered password should resolve as unique?

    Posted by Michael on January 30, 2012 Reply

  • Great post. But something’s not quite clear for me. When the user logs in, I can use “$password = $_SERVER[‘PHP_AUTH_PW’];”, so the password is sent safely.

    But when I let my users register, I should do the same hash on the client side before sending using POST? So the hash will be sent as plaintext. So any attacker intercepting these packages can read the hash, create a custom request and use the hash to login under the user’s name?

    Posted by Niek on January 31, 2012 Reply

  • I’m confused about this:
    // The first 64 characters of the hash is the salt

    Hasn’t the first 64 characters already been hash()’d 100000 times? How will using that another 100000 times successfully compare to the original hash that was salted with with a random string. (Yes, I don’t know how this stuff works, sorry).

    Posted by jfher on February 18, 2012 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="" highlight="">

Fork me on GitHub