Type Juggling
PHP’s == is one of the most reliable security bugs in the wild because
developers keep using it for things that look like equality but are actually
type coercion plus equality.
The one rule
==performs type juggling.===does not.
Everything in this page follows from that.
The classics
var_dump(0 == "abc"); // bool(true) on PHP < 8
var_dump(0 == ""); // bool(true) on PHP < 8
var_dump("1" == "01"); // bool(true)
var_dump("10" == "1e1"); // bool(true)
var_dump("100" == "1e2"); // bool(true)
var_dump(0 == null); // bool(true)On PHP 8.0+ the 0 == "abc" case was fixed. Most code in the wild still
runs on 7.4 or 8.0 with the old behaviour because of php.ini defaults or
because the developer relies on it without realising.
Magic hash collisions
This is the gold standard interview question. The pattern is:
if (md5($_POST['user_input']) == "0e123456789…") {
grant_admin();
}When both sides of == look like numeric strings starting with 0e, PHP
treats them as scientific notation: 0 * 10^123… = 0. Two different strings
whose MD5 hashes are both 0e<digits only> will compare equal.
md5("240610708") = "0e462097431906509019562988736854";
md5("QNKCDZO") = "0e830400451993494058024219903391";
// "240610708" == "QNKCDZO" → trueBoth hash to 0e… followed by only digits → both juggle to 0 → equal.
This breaks real auth checks. The “secret token compared with ==” pattern
shows up in WordPress plugins, custom admin endpoints, and one-off
installation tokens every month.
Array-vs-string juggling
$_GET['secret'] = "abc";
// vs
$_GET['secret'] = ["abc"];
if ($_GET['secret'] == $expected_string) { … }In PHP < 8, arbitrary array comparison with a string returned false
unless the array was empty — but strcmp($array, $string) returns NULL,
not an integer, and NULL == 0 is true. So:
if (strcmp($_GET['secret'], $real_secret) == 0) {
// bypass: send ?secret[]=anything
}strcmp chokes on arrays, returns NULL, NULL == 0 is true → bypass.
Detection during source review
Search patterns
# All loose comparisons + switch + array membership functions
rg -n '\b(==|!=|switch\()|in_array\(|array_search\(' --type=php
# Hash compared with == (magic-hash territory)
rg -n '(md5|sha1|hash)\s*\(' --type=php | rg ' == '
# strcmp / strcasecmp on attacker input
rg -n 'strcmp\(|strcasecmp\(' --type=phpRead the results carefully. Most are harmless ($i == count($arr) - 1). The
ones to flag are comparisons against user input or secrets.
Test inputs
Throw these at every endpoint that touches an == comparison on a
security-relevant value (token, role, id, coupon, signature, etc.):
| Payload | What it juggles to |
|---|---|
0 | int 0 — collides with strings that juggle to 0 |
false / null / "" | All compare equal to each other |
"0" | int 0 |
"0e12345" / "0e99999999" | scientific-notation zero, collides with 0e… hashes |
[] (e.g. ?secret[]=test) | array — breaks strcmp, returns NULL, juggles to 0 |
240610708 / QNKCDZO | classic magic-hash strings (see magic hash) |
Audit focus
For every ==, !=, switch(), in_array(), or array_search() hit:
- Does attacker-controlled input reach the comparison? Trace from
$_GET / $_POST / $_REQUEST / $_COOKIE / php://input / Authorization headerto the operator. - What kind of check is it? Authentication, authorization, API key, session token, password reset, payment validation, coupon validation, role/permission check, signature verification — all in scope.
- Can the input be coerced to a juggling-friendly type? Strings, arrays, and missing keys are the common ones.
- Is the comparison constant-time? Even when type-strict,
===on a secret is timing-attack-vulnerable. Switch tohash_equals()for tokens.
Reference incident: Wordfence ticket on a real-world == bypass.
The fix (for developers reading along)
===instead of==. Always, for security checks.hash_equals($known, $user)for token comparison — constant-time and type-strict.in_array($x, $a, true)instead of the default — see the next page.- Validate input type before comparing.
is_string($_POST['token'])first.
Quick reference
| Comparison | Result |
|---|---|
0 == "abc" (PHP < 8) | true |
0 == "" (PHP < 8) | true |
"1" == "01" | true |
"10" == "1e1" | true |
null == 0 | true |
null == false | true |
[] == false | true |
"abc" === 0 | false ✅ |
hash_equals("a", "b") | false ✅ (constant time) |