password_verify()
debt(d5/e1/b3/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep, phpstan, and psalm as tools. These are specialist static analysis tools — not a default linter or compiler error — that can detect hash comparison with === or == and absence of password_verify() usage. A default linter would not catch this, and it won't error at compile time, so d5 is the right anchor.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix is explicit: replace the === hash comparison with password_verify($input, $hash). Each misuse site is a single-call swap. No refactor, no cross-file changes required per fix.
Closest to 'localised tax' (b3). The applies_to contexts are web and api, and the tags show authentication/security scope. The choice affects authentication code specifically — typically one component or module — rather than the entire codebase. Future maintainers touching auth code must know to use password_verify(), but code outside auth is unaffected.
Closest to 'serious trap' (t7). The misconception field states directly that a competent developer would assume === is equivalent for hash comparison — which is the natural instinct from general string comparison patterns. The trap contradicts how comparison works elsewhere: === works fine for most equality checks, but silently fails here because the embedded salt means the same password produces different hashes each time, and timing leaks are introduced. This contradicts expected behavior from adjacent concepts like hash_equals() and general string equality.
Also Known As
TL;DR
Explanation
password_verify($plaintext, $hash) compares a user-supplied password against a stored hash created by password_hash(). It automatically extracts the algorithm, cost, and salt from the stored hash string and is timing-safe. Never compare password hashes with == or === — this is vulnerable to timing attacks and type juggling. password_verify() always returns bool and handles all supported PHP password algorithms transparently.
Common Misconception
Why It Matters
Common Mistakes
- Comparing hashes with === instead of password_verify() — the embedded salt means the same password produces different hashes.
- Not calling password_needs_rehash() after a successful verify — misses the opportunity to upgrade old hashes.
- Using password_verify() to compare non-password hashes like HMACs — use hash_equals() for those.
- Assuming password_verify() is timing-safe against all attacks — it is, but only use it for password_hash() outputs.
Code Examples
// Wrong — hashing again and comparing:
$inputHash = password_hash($input, PASSWORD_BCRYPT);
if ($inputHash === $storedHash) { /* Always false — different salts */ }
// Correct:
if (password_verify($input, $storedHash)) { /* Correct */ }
// Constant-time, extracts salt from hash automatically
$hash = $user->password; // from database
$input = $_POST['password'];
if (password_verify($input, $hash)) {
// Authenticated — check if rehash needed
if (password_needs_rehash($hash, PASSWORD_ARGON2ID, ['memory_cost' => 65536])) {
$new = password_hash($input, PASSWORD_ARGON2ID);
User::where('id', $user->id)->update(['password' => $new]);
}
} else {
// Wrong password — use identical code path as 'user not found' to prevent enumeration
abort(401, 'Invalid credentials');
}