The Complete Guide to PHP Type Juggling and Comparison Gotchas
Scott Keck-Warren • August 14, 2026
Years ago I inherited a login system that checked passwords with something close to this:
// don't use md5 for passwords!
if (md5($providedPassword) == $storedHash) {
// let them in
}
Most of the time it worked, but occasionally a user reported they could log in with the wrong password, and I spent an afternoon convinced I was losing my mind. The culprit turned out to be the ==. If both md5($providedPassword) and $storedHash happened to be "magic hashes," strings that look like 0e462097431906509019562988736854. PHP saw two strings that both start with 0e followed by all digits, decided they were numbers in scientific notation, and treated them as 0 == 0. Different passwords with the same result of bypassing our authentication.
The fix was to change == to ===. This caused PHP to "strictly" compare the strings instead of type-juggling them into integers.
Let me walk through what's happening so you never lose an afternoon to it as I did.
What type juggling does
PHP is loosely typed, so when you compare two values of different types with ==, it converts one (or both) of them to a common type before checking their equality. That conversion is type juggling, and it's really convenient until something doesn't work the way you expect.
== asks "are these equal after PHP massages the types?" === asks "are these the same type AND the same value?" The strict version does zero conversion, which is what you want almost every time.
0 == "0"; // true, string "0" converts to 0
0 === "0"; // false, int vs string, no conversion happens
The trouble is that "massages the types" hides a pile of rules most of us never memorized, and the rules changed in a big way.
PHP 8 fixed the worst offender
Before PHP 8, comparing a number to a non-numeric string converted the string to a number. Since a non-numeric string like "foo" converts to 0, you got this:
// PHP 7 and earlier
0 == "foo"; // true (!!)
Any integer 0 compared to any non-numeric string was true which wrecked security checks across the ecosystem. Imagine a token comparison where the stored value is 0:
// PHP 7: an attacker sends "anything" and this passes
if ($secretCode == $userInput) { ... }
PHP 8 flipped the rule so when you compare a number to a non-numeric string, PHP converts the number to a string instead:
// PHP 8
0 == "foo"; // false, becomes "0" == "foo"
Here's the part that bites people who think PHP 8 fixed everything: numeric strings still juggle. If the string looks like a number, the old behavior stands.
"1" == 1; // true, "1" is a numeric string
"10" == "1e1"; // true, both numeric, both equal 10
"0e1" == "0e2"; // true, still a magic-hash collision
The magic hash problem that ate my afternoon survives PHP 8, because both sides are numeric strings written in scientific notation. 0e1 and 0e2 both equal zero.
== vs === at a glance
| Feature | == (loose) |
=== (strict) |
|---|---|---|
| Compares value | Yes | Yes |
| Compares type | No | Yes |
| Converts types first | Yes | Never |
| Predictable across PHP versions | No, rules changed in 8.0 | Yes |
| Safe for tokens/secrets | No | Better, but use hash_equals() |
| Good default | No | Yes |
Surprising == results in PHP 8
Even with the PHP 8 improvements, these results catch people off guard. Every result below is current PHP 8 behavior.
| Comparison | Result | Why |
|---|---|---|
0 == "" |
false | Empty string is non-numeric, 0 becomes "0" |
0 == "0" |
true | "0" is numeric |
0 == "foo" |
false | Non-numeric string, 0 becomes "0" |
"1" == "01" |
true | Both numeric strings, compared as numbers |
"10" == "1e1" |
true | Both numeric, both equal 10 |
100 == "1e2" |
true | "1e2" is numeric, equals 100 |
null == false |
true | null juggles to false |
null == "" |
true | Both juggle to false |
[] == false |
true | Empty array juggles to false |
Notice how many of these still return true.
How to stop getting burned
Default to ===. You should always reach for strict comparison first and only loosen it when you have a specific reason. It's the same number of keystrokes, and it removes an entire category of bug.
Use hash_equals() for secrets
When you compare tokens, API keys, password hashes, or anything an attacker controls, use hash_equals($known, $userSupplied). It's built for this, it dodges the magic-hash trap, and it runs in constant time so you don't leak information through timing.
if (hash_equals($storedHash, md5($providedPassword))) { ... }
When you need coercion, be explicit.
If a value arrives from a form or query string and you want it as a number, cast it or validate it yourself instead of leaning on ==.
if (is_numeric($input) && (int) $input === 5) { ... }
Turn on strict mode in array functions.
in_array() and array_search() use loose comparison by default, which means in_array(0, ['foo', 'bar']) could surprise you.
Pass true as the third argument to force strict matching.
in_array($needle, $haystack, true);
array_search($needle, $haystack, true);
That third argument is the easiest safety win most codebases are missing.
What you need to know
==converts types before comparing;===never converts, so it's the safe default.- PHP 8 changed number-vs-string comparison so
0 == "foo"is nowfalse. Upgrade for the win. - Numeric strings still juggle, so
"1" == 1istrueand magic-hash collisions like"0e1" == "0e2"still bite. - Use
hash_equals()for tokens and password hashes, and passtruetoin_array()andarray_search().