Question

Secure Password Hashing in PHP: Salts, password_hash(), and Best Practices

phpsecurityhashpasswords

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:

  • bcrypt
  • Argon2id (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.

Take Quiz

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.

Take Quiz

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

OptionSuitable for passwords?SpeedSalt handlingRecommended?Notes
md5()NoVery fastManualNoObsolete for password storage
sha1()NoVery fastManualNoAlso too fast
hash('sha256', ...)NoFastManualNoBetter than MD5 for integrity, not for passwords
password_hash(..., PASSWORD_BCRYPT)YesIntentionally slowAutomaticYesStrong and widely supported
password_hash(..., PASSWORD_ARGON2ID)YesIntentionally slowAutomaticYesModern and strong when available
Manual repeated MD5/SHA loopsUsually noAdjustableManualNoEasy to implement badly

Salt vs no salt

ApproachResult
No saltSame password gives same hash every time
Random unique salt per passwordSame password gives different hashes for different users

Multiple hashes vs one strong hash

StrategyBetter?Why
Store MD5 and SHA1 togetherNoAttacker can break the weakest one
Store one password_hash() resultYesSimpler and stronger

bcrypt vs Argon2id

FeaturebcryptArgon2id
PHP supportVery commonAvailable in newer PHP versions
Good for passwordsYesYes
Automatic salt with password_hash()YesYes
Modern recommendationGoodOften preferred when available
Take Quiz

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() or sha256 directly 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_DEFAULT
  • PASSWORD_BCRYPT
  • PASSWORD_ARGON2ID if 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.

Take Quiz

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

  1. Accept a plain-text password and create a secure hash using PHP.
  2. Store the generated hash in a variable that simulates a database value.
  3. Verify one correct login attempt and one incorrect login attempt.
  4. Show how to detect whether the stored hash should be rehashed.
  5. Do not use MD5, SHA1, or manual salt generation.
Take Quiz

Keep learning

Related questions

View more