The Complete Guide to PHP Type Juggling and Comparison Gotchas cover image

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

  1. == converts types before comparing; === never converts, so it's the safe default.
  2. PHP 8 changed number-vs-string comparison so 0 == "foo" is now false. Upgrade for the win.
  3. Numeric strings still juggle, so "1" == 1 is true and magic-hash collisions like "0e1" == "0e2" still bite.
  4. Use hash_equals() for tokens and password hashes, and pass true to in_array() and array_search().