These notes are public, opinionated, and evolving — read abdelkader.ma for the long-form posts.
PHP SecurityType Juggling

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"  →  true

Both 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=php

Read 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.):

PayloadWhat it juggles to
0int 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 / QNKCDZOclassic magic-hash strings (see magic hash)

Audit focus

For every ==, !=, switch(), in_array(), or array_search() hit:

  1. Does attacker-controlled input reach the comparison? Trace from $_GET / $_POST / $_REQUEST / $_COOKIE / php://input / Authorization header to the operator.
  2. 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.
  3. Can the input be coerced to a juggling-friendly type? Strings, arrays, and missing keys are the common ones.
  4. Is the comparison constant-time? Even when type-strict, === on a secret is timing-attack-vulnerable. Switch to hash_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

ComparisonResult
0 == "abc" (PHP < 8)true
0 == "" (PHP < 8)true
"1" == "01"true
"10" == "1e1"true
null == 0true
null == falsetrue
[] == falsetrue
"abc" === 0false
hash_equals("a", "b")false ✅ (constant time)