Secure your PHP app by refactoring legacy password storage methods like MD5 and SHA1 to use password_hash() for strong encryption and protection.
If you’ve inherited an old PHP app, there’s a good chance its passwords are sitting in the database as md5() or sha1() hashes. That was normal practice years ago. It isn’t anymore. Those functions were built to be fast, and fast is exactly what you don’t want guarding a password. A modern GPU can grind through billions of guesses a second, and precomputed rainbow tables turn a stolen md5() hash back into plain text almost instantly.
The good news is that PHP already ships the fix. password_hash() gives you a slow, salted, self-describing hash in one function call, and password_verify() checks it safely. In this guide we’ll cover why the old approach fails, how the modern API works, and how to move an existing codebase over without forcing every user to reset their password.
Why Replace MD5 and SHA1 with password_hash()?
md5() and sha1() are fast, general-purpose hashing functions. That speed is the whole problem. They were designed to fingerprint data quickly, not to slow an attacker down. Store passwords with them and you inherit three real weaknesses:
- Rainbow table attacks: attackers keep huge precomputed tables that map hashes back to their inputs, so a common password reverses in seconds.
- Brute force attacks: because the algorithms are fast, an attacker can test billions of guesses per second on modern hardware.
- No built-in salt: on their own,
md5()andsha1()don’t salt anything, so identical passwords produce identical hashes, and one cracked hash unlocks every user who reused that password.
password_hash() answers all three. It uses bcrypt by default, which is deliberately slow and tunable, and it generates a unique random salt for every password automatically. The same password for two different users gives you two completely different hashes.
Understanding password_hash() and password_verify()
password_hash() turns a plain-text password into a secure hash. It supports several algorithms, and bcrypt is the default. Here’s the simplest call you can make:
Example: Hashing a Password Using password_hash()
<?php
$password = 'my_secure_password';
// Hash the password using bcrypt
$hashed_password = password_hash( $password, PASSWORD_BCRYPT );
// Output the hashed password
echo $hashed_password;The string you get back isn’t only the hash. It packs the algorithm, the cost factor, and the salt together in a single value. Store the whole thing and you never have to manage the salt separately.
To check a password later, hand the plain-text attempt and the stored hash to password_verify():
Example: Verifying a Password
<?php
$entered_password = 'my_secure_password';
// Check if the entered password matches the stored hash
if ( password_verify( $entered_password, $hashed_password ) ) {
echo 'Password is correct!';
} else {
echo 'Invalid password!';
}
password_verify() reads the algorithm, cost, and salt out of the stored hash, recomputes, and compares. The comparison runs in constant time, so it’s safe against timing attacks. Don’t ever compare hashes yourself with ===; let the function do it.
Refactoring Legacy Code to Use password_hash()
Migrating is mostly a find-and-replace job with a little care around your existing users. Let’s take it in order.
Step 1: Identify Password Hashing Code
Start by finding every place your app hashes or checks a password. Usually that’s your registration and login logic. In an older codebase it tends to look like this:
Example: Legacy Hashing Using MD5
<?php
// Hash the password using MD5
$password = 'my_legacy_password';
$hashed_password = md5( $password );
// Store $hashed_password in the databaseThat’s the code we’re replacing.
Step 2: Refactor to Use password_hash()
Swap the md5() call for password_hash(). The rest of the flow stays the same:
Example: Refactored Code with password_hash()
<?php
// Refactor to use password_hash() with bcrypt
$password = 'my_legacy_password';
$hashed_password = password_hash( $password, PASSWORD_BCRYPT );
// Store $hashed_password in the databaseThe password is now hashed with bcrypt instead of MD5, which is a night-and-day difference in how hard it is to crack. One limit worth knowing: with bcrypt, only the first 72 bytes of a password count, and anything past that is ignored. For ordinary passwords that’s a non-issue, but it’s a real ceiling, so don’t lean on very long passphrases to carry extra strength here.
Step 3: Updating the Authentication Logic
Registration is only half the job. Your login code has to change too. If it currently rehashes the input and compares strings, replace that with password_verify(). Here’s the old pattern:
Example: Legacy Password Verification
<?php
// Compare the hashed password (MD5)
if ( md5( $entered_password ) === $stored_hash ) {
echo 'Login successful';
} else {
echo 'Invalid password';
}
And the version that uses password_verify():
Example: Refactored Password Verification
<?php
// Compare using password_verify()
if ( password_verify( $entered_password, $stored_hash ) ) {
echo 'Login successful';
} else {
echo 'Invalid password';
}
Same shape, safer comparison. The stored hash already carries everything password_verify() needs to do its job.
Dealing with Existing Users and Passwords
Here’s the part that trips people up. You can’t un-hash your existing MD5 passwords to re-hash them with bcrypt. A hash only goes one way. So how do you migrate everyone without a mass password reset? You don’t do it all at once. You do it the next time each person logs in.
Step 4: Gradual Migration of Existing Users
At login, check whether the stored hash looks like an old MD5 hash. If it does and the password checks out, rehash it with password_hash() and save the new value. The user never notices:
Example: Gradual Password Migration
<?php
// Check if the hash is an old MD5 hash
if ( strlen( $stored_hash ) === 32 ) {
// Verify the old MD5 password
if ( md5( $entered_password ) === $stored_hash ) {
// Rehash the password using bcrypt
$new_hash = password_hash( $entered_password, PASSWORD_BCRYPT );
// Store the new hash in the database
// updateUserPassword( $user_id, $new_hash );
echo 'Password updated to bcrypt';
} else {
echo 'Invalid password';
}
} else {
// Use the new bcrypt verification method
if ( password_verify( $entered_password, $stored_hash ) ) {
echo 'Login successful';
} else {
echo 'Invalid password';
}
}
Every successful login quietly upgrades one more account. Over a few weeks, most of your active users are on bcrypt, and you never had to send a single “reset your password” email. The 32-character length check here is a rough way to spot a legacy MD5 hash. For hashes that were already produced by password_hash(), there’s a purpose-built function for deciding when to upgrade, which is next.
Best Practices for Password Storage
- Let
password_hash()pick the algorithm. PassingPASSWORD_DEFAULTuses bcrypt today, and it’s designed to track PHP’s current recommendation as stronger algorithms are added. If you have a specific reason,PASSWORD_BCRYPTorPASSWORD_ARGON2IDlet you pin an explicit choice. - Always verify with
password_verify(). Never compare hashes with===or hand-rolled logic. - Store the entire hash. The output already contains the algorithm, cost, and salt, so save all of it in a column with room to grow (
VARCHAR(255)is a safe bet). - Don’t roll your own crypto. The built-in functions are vetted and maintained. Anything you invent won’t be.
- Rehash when needed. After a successful
password_verify(), callpassword_needs_rehash()to see whether the hash was made with an older algorithm or a lower cost. If it was, rehash right there while you still hold the plain-text password.
Conclusion
Moving off md5() and sha1() isn’t a big rewrite. It’s a handful of focused changes: hash with password_hash(), check with password_verify(), and upgrade old hashes quietly at login. The API handles the hard parts, the salting, the slow bcrypt work, and the safe comparison, so you don’t have to.
Do this once and you close off a whole category of risk for your users. It’s a good afternoon’s work, and it’s the kind of thing future-you will be glad past-you handled.


