← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

password_verify()

PHP PHP 5.5+ Beginner
debt(d5/e1/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

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.

e1 Effort Remediation debt — work required to fix once spotted

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.

b3 Burden Structural debt — long-term weight of choosing wrong

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.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

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.

About DEBT scoring →

Also Known As

password_verify() PHP password check bcrypt verify

TL;DR

Checks a plaintext password against a bcrypt/Argon2 hash produced by password_hash() — the correct way to validate passwords.

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

password_verify() can be replaced with hash comparison using ===. password_verify() performs a constant-time comparison internally. Using === leaks timing information, and also fails to account for future algorithm upgrades handled automatically by password_needs_rehash().

Why It Matters

password_verify() extracts the algorithm and salt from the stored hash and recomputes it — it is the only correct way to verify a password_hash() result because the salt is embedded.

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

✗ Vulnerable
// 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 */ }
✓ Fixed
// 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');
}

Added 15 Mar 2026
Edited 22 Mar 2026
Views 69
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 1 ping W 2 pings T 0 pings F 4 pings S 1 ping S 2 pings M 0 pings T 3 pings W 1 ping T 0 pings F 1 ping S 1 ping S 2 pings M 0 pings T 0 pings W 1 ping T 0 pings F 2 pings S 0 pings S 1 ping M 3 pings T 0 pings W 1 ping T 3 pings F 2 pings S 1 ping S 0 pings M 2 pings T 0 pings W
No pings yet today
ChatGPT 2
ChatGPT 25 Amazonbot 10 Perplexity 5 Google 4 Ahrefs 4 Unknown AI 3 SEMrush 3 Scrapy 3 Majestic 2 Claude 2 Bing 2 Meta AI 1 PetalBot 1
crawler 58 crawler_json 6 pre-tracking 1
DEV INTEL Tools & Severity
🔴 Critical ⚙ Fix effort: Low
⚡ Quick Fix
Use password_verify($input, $hash) — it's constant-time and handles the salt embedded in the bcrypt/argon2 hash automatically; never compare hashes with == or ===
📦 Applies To
PHP 5.5+ web api
🔗 Prerequisites
🔍 Detection Hints
Hash comparison with === or ==; md5($password) comparison; SHA1 for password storage; no password_verify() usage
Auto-detectable: ✓ Yes semgrep phpstan psalm
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Low ✓ Auto-fixable Fix: Low Context: Line Tests: Update
CWE-916


✓ schema.org compliant