Question
Secure Password Hashing in PHP: Salts, password_hash(), and Best Practices
Question
I understand that MD5 is no longer considered safe for password storage. Given that, what mechanism should I use to protect passwords in PHP?
I have seen conflicting advice:
- Some discussions suggest hashing a password multiple times.
- Others recommend using a salt.
I want to build a password storage system in PHP that is both secure and reasonably fast. A very large number of hash iterations may improve security, but it also makes authentication slower. How do I choose a good balance between speed and security?
I also prefer the stored result to have a consistent format or length if possible.
My requirements are:
- The hashing mechanism must be available in PHP.
- It must be secure for password storage.
- It may use a salt, but if so:
- Are all salts equally good?
- How should a good salt be generated?
I also want to know whether storing multiple hashes for the same password, such as one MD5 hash and one SHA hash, would make the system more secure or less secure.
In short, I want to know:
- Which password hashing function(s) should be used in PHP
- Whether salting is necessary
- How to generate a good salt
- Whether multiple different hashes of the same password improve security
Short Answer
By the end of this page, you will understand how passwords should be stored securely in PHP, why MD5 and plain SHA hashes are not suitable, how salts work, why modern password hashing functions are designed to be slow on purpose, and how to use password_hash() and password_verify() correctly in real applications.
Concept
What problem are we solving?
When a user creates a password, you should not store the original password in your database. Instead, you store a password hash.
A hash is a one-way transformation:
- You can hash a password and store the result.
- Later, when the user logs in, you hash the entered password again and compare it.
- You should not be able to reverse the hash back into the original password.
Why MD5 and SHA are not enough
Functions like MD5, SHA1, and even SHA256 are fast general-purpose hashing algorithms. That is good for checksums and file integrity, but bad for password storage.
Why bad?
- Attackers can test huge numbers of guesses very quickly.
- Modern GPUs and specialized hardware can crack fast hashes efficiently.
- Precomputed lookup tables and large wordlists make attacks easier.
Password hashing should be slow by design.
What makes a password hash secure?
A secure password storage system should have these properties:
- One-way: the original password is not stored.
- Slow to compute: brute-force attacks become expensive.
- Salted: each password gets its own random salt.
- Upgradeable: you can increase the cost over time.
What is a salt?
A salt is a random value added to a password before hashing.
This means:
- Two users with the same password will have different stored hashes.
- Attackers cannot rely on precomputed rainbow tables.
- Each password has to be attacked individually.
A good salt must be:
- Random
- Unique per password
- Generated using a secure random source
Should you hash multiple times yourself?
Repeated hashing by hand is usually the wrong approach unless you are implementing a well-designed password hashing algorithm. A custom loop like this is not recommended:
$hash = md5($password);
for ($i = 0; $i < 1000000; $i++) {
$hash = md5($hash);
}
This is still built on a fast hash function and is easy to implement incorrectly.
Instead, use a password hashing algorithm made specifically for passwords, such as:
bcryptArgon2id(preferred when available)
In PHP, the safest beginner-friendly choice is to use the built-in API:
password_hash()password_verify()password_needs_rehash()
These handle the salt automatically.
Should you store MD5 and SHA together?
No. Storing multiple hashes of the same password usually makes things worse, not better.
Why?
- An attacker can attack the weakest stored hash.
- If one hash is easy to brute-force, the password is exposed.
- Multiple hashes do not create meaningful extra protection.
Security should come from one strong password hashing scheme, not several weak or unrelated ones.
Best practice in modern PHP
Use:
password_hash($password, PASSWORD_DEFAULT)
Then verify with:
password_verify($password, $storedHash)
PASSWORD_DEFAULT uses a strong algorithm chosen by PHP. In many environments today, this is bcrypt, and in some cases newer defaults may change over time. That is good because it allows your application to improve automatically as PHP evolves.
If your PHP version supports it and your environment allows it, Argon2id is a strong modern choice.
About constant length
The output format of password hashes is typically structured text that includes:
- the algorithm
- the cost settings
- the salt
- the hash itself
So the stored value may have a predictable format for a given algorithm, but you should not optimize your design around “short” or manually fixed-length hashes. Instead, allocate enough space in your database, for example a VARCHAR(255), which is a common safe choice for modern password hashes.
Mental Model
Think of password hashing like storing a locked box label instead of the key itself.
- The password is the key.
- The hash is a label created from that key.
- The salt is like adding a unique serial number before making the label.
If two people have the same key, the serial number makes their labels different.
A fast hash like MD5 is like a machine that prints labels instantly. That sounds useful, but it also helps attackers print billions of labels while guessing passwords.
A password hashing algorithm like bcrypt or Argon2 is like a machine intentionally built to work more slowly and carefully. That delay is small for a real login, but expensive for an attacker trying millions of guesses.
Syntax and Examples
Recommended PHP syntax
Hashing a password
<?php
$password = 'MySecureP@ssw0rd!';
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash;
Verifying a password
<?php
$enteredPassword = 'MySecureP@ssw0rd!';
$storedHash = '$2y$10$exampleexampleexampleexampleexampleexampleexampleexam';
if (password_verify($enteredPassword, $storedHash)) {
echo 'Password is correct';
} else {
echo 'Invalid password';
}
Better example with real flow
Registering a user
<?php
$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_DEFAULT);
// Store $hash in the database
Logging in
<?php
$password = $_POST['password'];
$storedHash = $userFromDatabase['password_hash'];
if (password_verify($password, $storedHash)) {
echo 'Login successful';
} else {
echo 'Login failed';
}
Why this is better
- You do not manually create the salt.
- You do not manually combine password and salt.
- You do not use MD5 or SHA directly.
- PHP stores the algorithm details inside the resulting hash string.
Rehashing when needed
As hardware changes, you may want stronger settings later.
<?php
if (password_verify($password, $storedHash)) {
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// Save $newHash to the database
}
}
This lets your system upgrade password hashes over time without forcing all users to reset their passwords.
Step by Step Execution
Example
<?php
$password = 'cat123';
$hash = password_hash($password, PASSWORD_DEFAULT);
$isValid = password_verify('cat123', $hash);
var_dump($isValid);
What happens step by step
1. The password is defined
$password = 'cat123';
The variable holds the plain-text password.
2. PHP creates a secure password hash
$hash = password_hash($password, PASSWORD_DEFAULT);
PHP does several things internally:
- Chooses the current default password hashing algorithm
- Generates a secure random salt
- Combines the password and salt correctly
- Applies the algorithm with the proper cost settings
- Returns a full hash string containing the metadata needed later
3. The password is checked
$isValid = password_verify('cat123', $hash);
PHP reads the stored hash and extracts the algorithm settings and salt from it. Then it hashes the entered password the same way and compares the result securely.
4. The result is printed
var_dump($isValid);
Output:
bool(true)
If the password were wrong:
$isValid = password_verify('wrong-password', $hash);
Then the result would be:
bool(false)
Key takeaway
You store only the hash, not the original password and not a separate manually generated salt in most modern PHP password APIs.
Real World Use Cases
User authentication
The most common use case is login systems:
- registration forms
- sign-in pages
- admin dashboards
- membership systems
API and developer portals
If your platform has user accounts for accessing API keys or dashboards, the passwords for those accounts should be stored with password_hash().
Internal business tools
Even small internal apps need proper password storage:
- employee portals
- HR dashboards
- reporting systems
- support tools
Password resets and account migration
During a login, you can verify an old stored hash and rehash it using stronger settings. This is useful when improving security in existing apps.
Multi-tenant SaaS applications
Different users may choose the same password. Salted password hashing prevents them from ending up with identical stored hashes, which reduces information leakage if the database is exposed.
Real Codebase Usage
Typical pattern in real projects
Developers usually build password hashing into these layers:
- registration service
- login/authentication service
- user model or repository
- password reset flow
Common patterns
Validation before hashing
<?php
if (strlen($password) < 12) {
throw new InvalidArgumentException('Password must be at least 12 characters.');
}
$hash = password_hash($password, PASSWORD_DEFAULT);
Passwords should be validated before hashing, not after.
Guard clause for failed login
<?php
if (!$user) {
return false;
}
if (!password_verify($password, $user['password_hash'])) {
return false;
}
return true;
This keeps authentication logic simple and readable.
Rehash on successful login
<?php
if (password_verify($password, $user['password_hash'])) {
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// update database
}
}
This is a common long-term maintenance pattern.
Error handling
Real projects separate concerns:
- input validation
- authentication logic
- database persistence
- session creation
The password hashing API should be used only for password storage and verification, not for encryption, tokens, or file integrity checks.
Database design
A common approach is a single column such as:
password_hash VARCHAR(255) NOT NULL
That gives enough room for current and future password hash formats.
Common Mistakes
1. Using MD5 or SHA directly for passwords
Broken approach:
<?php
$hash = md5($password);
Or:
<?php
$hash = hash('sha256', $password);
Why it is a problem:
- They are too fast.
- They are not designed for password storage.
- Attackers can brute-force them efficiently.
Use this instead:
<?php
$hash = password_hash($password, PASSWORD_DEFAULT);
2. Making your own salt incorrectly
Broken approach:
<?php
$salt = '12345';
$hash = hash('sha256', $salt . $password);
Problems:
- Salt is predictable.
- Salt may be reused for all users.
- You are still using a fast hash.
Avoid manual salt handling when using password_hash().
3. Using the same salt for every user
If every user shares one global salt, attackers still gain large efficiency advantages.
Each password needs its own unique random salt.
4. Storing multiple hashes of the same password
Broken idea:
<?php
$md5 = md5($password);
$sha1 = sha1($password);
This does not combine their strengths. It gives attackers more material and lets them target the weakest option.
5. Confusing hashing with encryption
Hashing:
- one-way
- used for password verification
Encryption:
- two-way
- used when data must later be decrypted
Passwords should usually be hashed, not encrypted.
6. Choosing output length over security
A shorter or fixed-size output is not the main goal. Security matters more than having a neat-looking string.
Design your database to fit secure hashes instead of weakening the algorithm to fit a smaller column.
7. Writing custom password hashing logic
Even if it seems simple, custom security code is easy to get wrong. Prefer battle-tested built-in APIs.
Comparisons
Password hashing choices in PHP
| Option | Suitable for passwords? | Speed | Salt handling | Recommended? | Notes |
|---|---|---|---|---|---|
md5() | No | Very fast | Manual | No | Obsolete for password storage |
sha1() | No | Very fast | Manual | No | Also too fast |
hash('sha256', ...) | No | Fast | Manual | No | Better than MD5 for integrity, not for passwords |
password_hash(..., PASSWORD_BCRYPT) | Yes | Intentionally slow | Automatic | Yes | Strong and widely supported |
password_hash(..., PASSWORD_ARGON2ID) | Yes | Intentionally slow | Automatic | Yes | Modern and strong when available |
| Manual repeated MD5/SHA loops | Usually no | Adjustable | Manual | No | Easy to implement badly |
Salt vs no salt
| Approach | Result |
|---|---|
| No salt | Same password gives same hash every time |
| Random unique salt per password | Same password gives different hashes for different users |
Multiple hashes vs one strong hash
| Strategy | Better? | Why |
|---|---|---|
| Store MD5 and SHA1 together | No | Attacker can break the weakest one |
Store one password_hash() result | Yes | Simpler and stronger |
bcrypt vs Argon2id
| Feature | bcrypt | Argon2id |
|---|---|---|
| PHP support | Very common | Available in newer PHP versions |
| Good for passwords | Yes | Yes |
Automatic salt with password_hash() | Yes | Yes |
| Modern recommendation | Good | Often preferred when available |
Cheat Sheet
Best practice
$hash = password_hash($password, PASSWORD_DEFAULT);
$isValid = password_verify($password, $hash);
Rules to remember
- Do not use
md5()for passwords. - Do not use
sha1()orsha256directly for passwords. - Do not store plain-text passwords.
- Do not build your own salt system unless you truly need to.
- Do use
password_hash(). - Do use
password_verify(). - Do use
password_needs_rehash()over time. - Do give your database column enough space, such as
VARCHAR(255).
Good salt rules
- random
- unique per password
- generated with a secure source
- usually handled automatically by
password_hash()
Good database field
password_hash VARCHAR(255)
Modern algorithms in PHP
PASSWORD_DEFAULTPASSWORD_BCRYPTPASSWORD_ARGON2IDif available
Never do this
md5($password)
sha1($password)
hash('sha256', $password)
for password storage.
Upgrade pattern
if (password_verify($password, $storedHash)) {
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$storedHash = password_hash($password, PASSWORD_DEFAULT);
}
}
FAQ
Should I use MD5 for passwords in PHP?
No. MD5 is too fast and no longer suitable for password storage.
Is SHA-256 safe for password hashing?
Not by itself for password storage. It is a strong general-purpose hash, but it is too fast for defending against brute-force attacks.
Do I need to generate my own salt in PHP?
Usually no. password_hash() generates and stores a secure salt automatically.
Is bcrypt still acceptable in PHP?
Yes. Bcrypt is still a solid choice and is widely supported. Argon2id is also an excellent option when available.
Why are password hashes intentionally slow?
Because slow hashing makes brute-force attacks more expensive while keeping normal login checks practical.
Should I store several hashes of the same password for extra security?
No. That usually weakens security because an attacker can target the easiest hash.
How long should the database column be for a password hash?
A common practical choice is VARCHAR(255) so future algorithm changes are less likely to cause problems.
Is password hashing the same as encryption?
No. Hashing is one-way and is used for password verification. Encryption is two-way and is used when data must be recovered later.
Mini Project
Description
Build a small PHP password registration and login example that stores a secure password hash and verifies it during login. This demonstrates the correct use of PHP's built-in password hashing API without using MD5, SHA1, or custom salts.
Goal
Create a simple PHP script that can hash a new password, store the hash in memory, and verify login attempts against it securely.
Requirements
- Accept a plain-text password and create a secure hash using PHP.
- Store the generated hash in a variable that simulates a database value.
- Verify one correct login attempt and one incorrect login attempt.
- Show how to detect whether the stored hash should be rehashed.
- Do not use MD5, SHA1, or manual salt generation.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.