Skip to content

NotPwnedVerifier: loose comparison causes false positives on magic hash passwords #59634

@ruttydm

Description

@ruttydm

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

  1. User sets password "aaroZmOk" (or any password with a magic SHA-1 hash)
  2. NotPwnedVerifier sends prefix 0E665 to the haveibeenpwned API
  3. API returns all breached hash suffixes matching that prefix
  4. 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
  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions