Summary
NotPwnedVerifier::verify() uses loose comparison (==) to match password hashes against the haveibeenpwned API response. This triggers PHP's numeric string type juggling on "magic hash" passwords (SHA-1 hashes matching scientific notation), causing false-positive "compromised password" rejections.
Version: 13.3.0
Vulnerable code
NotPwnedVerifier.php line 58:
return $hashPrefix.$hashSuffix == $hash && $count > $threshold;
Root cause
Some passwords produce SHA-1 hashes where every character is a digit (0–9) and the hash starts with 0E. PHP treats these strings as scientific notation (0 × 10^N = 0). When both sides of == are numeric strings, PHP compares them as numbers instead of strings.
Known example:
$hash = strtoupper(sha1("aaroZmOk"));
// "0E66507019969427134894567494305185566735" — all digits after 0E
is_numeric($hash); // true — PHP sees this as 0 × 10^665...
How it triggers
- User sets password
"aaroZmOk" (or any password with a magic SHA-1 hash)
NotPwnedVerifier sends prefix 0E665 to the haveibeenpwned API
- API returns all breached hash suffixes matching that prefix
- For any API entry that is ALSO an all-digit string, the comparison becomes:
"0E66500000000000000000000000000000000000" == "0E66507019969427134894567494305185566735"
// Both are numeric strings → compared as numbers → 0 == 0 → TRUE
- Password is incorrectly rejected as "compromised" even if it is not in any breach
This behavior persists in PHP 8+ — == between two numeric strings still uses numeric comparison.
Impact
Users whose passwords produce magic SHA-1 hashes cannot set those passwords — the validation falsely reports them as compromised. This is a denial-of-service on specific passwords.
Fix
return $hashPrefix.$hashSuffix === $hash && $count > $threshold;
Strict comparison (===) always does string comparison for two strings, regardless of whether they look numeric.
Summary
NotPwnedVerifier::verify()uses loose comparison (==) to match password hashes against the haveibeenpwned API response. This triggers PHP's numeric string type juggling on "magic hash" passwords (SHA-1 hashes matching scientific notation), causing false-positive "compromised password" rejections.Version: 13.3.0
Vulnerable code
NotPwnedVerifier.phpline 58:Root cause
Some passwords produce SHA-1 hashes where every character is a digit (0–9) and the hash starts with
0E. PHP treats these strings as scientific notation (0 × 10^N = 0). When both sides of==are numeric strings, PHP compares them as numbers instead of strings.Known example:
How it triggers
"aaroZmOk"(or any password with a magic SHA-1 hash)NotPwnedVerifiersends prefix0E665to the haveibeenpwned APIThis behavior persists in PHP 8+ —
==between two numeric strings still uses numeric comparison.Impact
Users whose passwords produce magic SHA-1 hashes cannot set those passwords — the validation falsely reports them as compromised. This is a denial-of-service on specific passwords.
Fix
Strict comparison (
===) always does string comparison for two strings, regardless of whether they look numeric.