password_verify

Checks whether a plain-text password matches a hash created by password_hash.

Signature
password_verify(#[\SensitiveParameter] string $password, string $hash): bool
  • Added in PHP 5.5.0

Parameters

ParameterTypeDescription
$passwordstringThe user's plain-text password, exactly as submitted.
$hashstringA hash created by password_hash().

Return value

booltrue if the password matches the hash, false otherwise.

Errors

Does not throw. An unrecognised or malformed hash simply returns false.

password_verify is the only correct way to check a password against a hash produced by password_hash. It reads the algorithm, cost and salt out of the stored hash, re-derives a hash from the submitted password using exactly those parameters, and compares the two in constant time.

That constant-time comparison matters. Comparing with == or === leaks timing information about how many bytes matched, and == in particular has historically produced false positives on hash-like strings. Never re-implement this by hashing the input yourself and comparing.

What it does not do

It never tells you why a check failed. A wrong password, a corrupted hash column, an empty string and an MD5 left over from an old system all return the same false. If logins break after a schema change, inspect the stored value with password_get_info rather than assuming users forgot their passwords.

Pass the submitted password through unchanged. Trimming it, lowercasing it, or running htmlspecialchars on it before verification will break accounts whose password contains the affected characters — and will do so only for some users, which makes it a miserable bug to track down.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Authenticate a user at login against the hash stored in their row
  • Confirm the current password before allowing a password or email change
  • Check an admin token or invite code that was stored hashed rather than in plain text
  • Re-authenticate before a destructive action such as account deletion

Frequently asked questions

Why can't I just compare password_hash output with ===?

password_hash generates a new random salt every call, so hashing the same password twice never produces the same string. password_verify extracts the original salt from the stored hash and compares in constant time, which string comparison cannot do safely.

Does password_verify work with hashes made by crypt?

Yes, for crypt hashes using a format PHP recognises, such as the $2y$ bcrypt format. It will not verify plain MD5 or SHA-1 digests, which return false.

Is password_verify safe against timing attacks?

Yes. The internal comparison runs in constant time regardless of how many bytes match, so an attacker learns nothing from response timing about how close a guess was.

More examples

Avoid leaking which usernames exist

All three calls do the same amount of bcrypt work. Without the dummy hash, a missing user would return instantly while a real user costs 50 ms, and an attacker could time the difference to enumerate valid accounts. The results are true, false, false.

Example · php
<?php
$users = ['ana' => password_hash('rightpass', PASSWORD_BCRYPT, ['cost' => 5])];
$dummy = password_hash('never-matches', PASSWORD_BCRYPT, ['cost' => 5]);

function attempt(array $users, string $dummy, string $name, string $pass): bool
{
    $hash = $users[$name] ?? $dummy;
    return password_verify($pass, $hash);
}

var_dump(attempt($users, $dummy, 'ana', 'rightpass'));
var_dump(attempt($users, $dummy, 'ana', 'wrong'));
var_dump(attempt($users, $dummy, 'ghost', 'anything'));

A truncated column looks exactly like a wrong password

The correct password fails against the truncated hash, and password_verify gives no hint why. password_get_info exposes the real cause: the shortened string is no longer recognised as bcrypt, so algo comes back null and algoName is "unknown".

Example · php
<?php
$hash = password_hash('hunter2', PASSWORD_BCRYPT, ['cost' => 4]);
$truncated = substr($hash, 0, 40); // as if stored in VARCHAR(40)

var_dump(password_verify('hunter2', $hash));
var_dump(password_verify('hunter2', $truncated));

print_r(password_get_info($truncated));

Discussion

  • Be the first to comment on this lesson.