PHP Security

PHP Type Juggling & Loose Comparisons

Youssef
17 May 2026

PHP Type Juggling & Loose Comparisons

The Root Cause

PHP is a weakly typed language. The == operator does not compare types — it converts both operands to a common type first, then compares values. This "type juggling" behavior leads to surprising and exploitable results, especially in authentication and token validation code.

🔴

Critical Note: PHP 7 and below are much more vulnerable to type juggling. PHP 8 changed the behavior of == when comparing strings to integers, fixing many classic attacks. Always check the PHP version.

== vs === — The Key Difference

// == (loose) — performs type coercion
var_dump(0 == "a");          // TRUE  in PHP 7, FALSE in PHP 8
var_dump(0 == "");           // TRUE  in PHP 7, FALSE in PHP 8
var_dump(0 == "0");          // TRUE  in both
var_dump("1" == "01");       // TRUE  (both numeric strings)
var_dump("10" == "1e1");     // TRUE  (1e1 = 10)
var_dump(100 == "1e2");      // TRUE  (1e2 = 100)
var_dump(true == "admin");   // TRUE
var_dump(null == false);     // TRUE
var_dump(null == 0);         // TRUE

// === (strict) — checks type AND value
var_dump(0 === "a");         // FALSE — always safe
var_dump("1" === "01");      // FALSE — different strings

Magic Hashes — The Classic Attack

PHP's loose comparison treats strings starting with 0e followed by digits as scientific notation — i.e., 0 × 10^n = 0. If two different MD5 hashes both start with 0e[digits], they compare as equal!

// "Magic" MD5 hashes — all equal 0 under ==
md5("240610708")   = "0e462097431906509019562988736854"
md5("QNKCDZO")    = "0e830400451993494058024219903391"
md5("aabg7XSs")   = "0e087386482136013740957780965295"

// SHA1 magic hashes
sha1("aaroZmOk")  = "0e66507019969427134894567494305185566735"
sha1("aaK1STfY")  = "0e76658526655756207688271159624026011393"

// Attack scenario:
$hash = "0e462097431906509019562988736854"; // stored in DB for user
if (md5($_POST['password']) == $hash) {      // loose comparison!
    // login success
}
// Send password = "QNKCDZO" → md5 = "0e830..." == "0e462..." → TRUE → bypassed!

strcmp() Bypass

In PHP, strcmp() returns 0 if equal, non-zero if different. When an array is passed instead of a string, strcmp() returns NULL — and NULL == 0 is true!

// VULNERABLE
if (strcmp($_POST['password'], $secret) == 0) {
    // logged in
}

// Attack: send password as array
// POST: password[]=anything
// strcmp(["anything"], "secret") → NULL → NULL == 0 → TRUE → bypassed!

in_array() Loose Comparison

// in_array() uses == by default
$whitelist = [1, 2, 3, 4];
var_dump(in_array("1 malicious string", $whitelist)); // TRUE! "1 malicious" == 1

// Also dangerous:
$blocked = ["admin", "root"];
var_dump(in_array(0, $blocked)); // TRUE in PHP 7! 0 == "admin" → TRUE

// SAFE — use strict mode (third parameter)
in_array($value, $whitelist, true); // strict comparison

switch() Type Juggling

// switch uses == internally
$role = $_GET['role']; // attacker sends: "0"

switch ($role) {
    case "admin":
        grant_admin();  // 0 == "admin" → TRUE in PHP 7!
        break;
    case "user":
        grant_user();
        break;
}

JSON Deserialization Type Confusion

// JSON decode returns typed values
$data = json_decode('{"admin": true}', true);
$data = json_decode('{"admin": 1}', true);

// Vulnerable check
if ($data['admin'] == "true") { /* true == "true" → TRUE */ }

// Attack via JSON: send {"password": true}
// if (json['password'] == $hash) → true == "0e123..." → TRUE in PHP 7

Prevention

// ALWAYS use strict comparison ===
if ($password === $stored_hash) { }

// ALWAYS use strict in_array
in_array($val, $arr, true);

// Use password_verify() for passwords — never raw == comparison
if (password_verify($_POST['password'], $stored_hash)) { }

// Use hash_equals() for token comparison — also timing-safe
if (hash_equals($token, $_GET['token'])) { }

// strcmp with ===
if (strcmp($a, $b) === 0) { }

Rule: Always use === in security-sensitive comparisons. Never use == to compare passwords, tokens, hashes, or roles. Use password_verify() and hash_equals() where applicable.