FlawOpen

flawopen.com/Type Juggling/PHP ==

Is PHP's == safe for comparing hashes?

CWE-697: Incorrect ComparisonReference page
Short answer

No. With ==, PHP compares two numeric strings numerically, so distinct strings such as scientific-notation hashes can compare equal even on PHP 8. Validate request types before use. For exact values use ===; for secret tokens or MACs use hash_equals() to avoid content-dependent timing leakage.

VULNERABLE
<?php
// Both strings are numeric and represent zero
$storedHash = "0e462097431906509019562988736854";
$submittedHash = "0e987654321";

// == returns true on PHP 7 and PHP 8
if ($storedHash == $submittedHash) {
    login();
}

// Request data may also have the wrong type:
$submitted = $_POST['token'] ?? null;
if ($storedHash == $submitted) {
    login();
}
// ?token[]=x supplies an array, not a string.
FIXED
<?php
$submitted = $_POST['token'] ?? null;
if (!is_string($submitted)) {
    http_response_code(400);
    exit;
}

// For secret tokens or MACs:
$tokenIsValid = hash_equals($storedToken, $submitted);
if ($tokenIsValid) {
    login();
}

// For non-secret identifiers, strict
// comparison checks both type and value:
$idsMatch = ($expectedId === $submitted);

// Passwords require password_verify(),
// not a hand-written hash comparison:
$passwordIsValid = password_verify($plain, $storedPasswordHash);

The "magic hash" problem

Two numeric strings have been compared numerically by PHP's loose string comparison before and after PHP 8. A hash whose hexadecimal output starts with 0e and continues only with digits is a valid scientific-notation string whose numeric value is zero. For example, two different strings such as 0e462097431906509019562988736854 and 0e987654321 both represent zero, so they compare equal with ==. PHP 8 changed string-to-number comparisons, not this numeric-string-to-numeric-string case.

PHP 8 changed comparisons between a number and a string that is not numeric: PHP now compares their string forms instead of treating the non-numeric string as zero. It did not remove the classic comparison between two numeric 0e… strings. Do not use == for authentication, regardless of PHP version.

Request types are part of the boundary

Request parameters are not guaranteed to be strings. A query such as ?token[]=x produces an array. Passing it into a string-oriented API can raise a type error or take an unintended code path, depending on the PHP version and function. Require a string at the request boundary before comparing or passing it to hash_equals().

The durable defense has two parts: validate input type at the boundary, then use a comparison whose semantics match the security requirement. === checks type and value; hash_equals() checks secret strings without leaking content-dependent timing.

FAQ

Does PHP 8 fix this entirely?

No. PHP 8 changed string-to-number comparisons, but comparisons between two numeric strings still use numeric semantics. Distinct 0e… strings can still compare equal. Use === for exact typed values or hash_equals() for secrets, and validate input types on every supported version.

Why hash_equals() rather than ===?

PHP documents hash_equals() as timing-attack safe for string contents; ordinary strict comparison can take different time depending on where strings differ. Pass the known secret first and the user-supplied string second. Passwords should use password_verify().

Do other languages have this?

Loose-comparison surprises exist in JavaScript's == too, which is why === is the recommended default there. The specific magic-hash form is a PHP artefact of hex hashes plus scientific-notation parsing.

References

See also Is Rust's unsafe block still memory safe? Is strncpy() safer than strcpy()? CWE vs. CVE: What's the Difference? Mass Assignment: Risks and Safe Field Allowlists Serverless State Bleed: Keep User Data Request-Scoped