flawopen.com/Type Juggling/PHP ==
Is PHP's == safe for comparing hashes?
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.
<?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.
<?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
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.
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().
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.