flawopen.com/Type Juggling/PHP ==

Is PHP's == safe for comparing hashes?

CWE-697: Incorrect ComparisonReference page
Short answer

No. PHP's loose comparison converts operands before comparing them, so two different strings can be "equal". For authentication checks use === (strict, compares type and value) or, for secrets, hash_equals(), which is also constant-time.

VULNERABLE
<?php
// Loose comparison of a stored hash
if ($storedHash == $submittedHash) {
    login();
}

// In PHP < 8, two "numeric" strings are
// compared as numbers. Hashes beginning
// 0e followed by only digits parse as
// scientific notation = 0.
//   "0e123456" == "0e987654"  → true

// And a JSON body can send an array:
if ($token == $_POST['token']) { ... }
// token[]=x makes the comparison
// behave unexpectedly rather than fail
// closed.
FIXED
<?php
// Strict: compares type AND value
if ($storedHash === $submittedHash) { ... }

// For secrets, also constant-time:
if (hash_equals($storedHash, $submitted)) {
    login();
}

// For passwords, never compare hashes
// by hand at all:
if (password_verify($plain, $storedHash)) {
    login();
}

// Validate the type before comparing
if (!is_string($_POST['token'])) {
    abort(400);
}

The "magic hash" problem

Before PHP 8, comparing two strings with == where both looked numeric caused both to be converted to numbers. A hash whose hexadecimal output happens to start with 0e and continue with digits only — for example 0e462097431906509019562988736854 — parses as "0 raised to the power of …", which is 0. Any two such hashes therefore compared as equal, so an attacker who could find one input hashing to a magic-hash form could match any other.

PHP 8 changed the rules: a string-to-number comparison now only converts when the string is genuinely numeric, and otherwise compares as strings. That removes the classic magic-hash bypass — but it does not make == a correct tool for security comparisons, and a great deal of PHP still runs on older branches.

Array and null tricks

The second half of this bug class is type confusion from request input. A query string like ?token[]= produces an array, not a string. Code that assumes a string and compares loosely can behave in ways the author never considered, and functions such as strcmp() historically returned null on array input — which itself loosely equals 0, the "strings match" return value.

The durable defence is to validate the type at the boundary before any comparison happens, rather than relying on the comparison operator to fail safely.

FAQ

Does PHP 8 fix this entirely?

It fixes the magic-hash case by changing string-to-number comparison semantics. It does not make loose comparison appropriate for authentication, and it does not address array-type confusion in input. Use === or hash_equals() regardless of version.

Why hash_equals() rather than ===?

=== short-circuits on the first differing byte, which leaks timing information about how much of a secret matched. hash_equals() compares in constant time, which matters for tokens and MACs.

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