VNX-PHP-010 – PHP Type Juggling in Comparison
Overview
This rule flags PHP code where a superglobal value ($_GET, $_POST, $_REQUEST, $_COOKIE) is compared using the loose equality operator (== or !=) rather than the strict equality operator (=== or !==). PHP’s loose comparison performs type coercion before comparing values, and the coercion rules produce counterintuitive results that attackers can exploit to bypass authentication, skip access checks, and forge token comparisons. This maps to CWE-697: Incorrect Comparison.
Severity: High | CWE: CWE-697 – Incorrect Comparison
Why This Matters
PHP’s type juggling is one of the most misunderstood aspects of the language’s runtime. The == operator converts both operands to a common type before comparing, and the conversion rules create a class of authentication bypasses that look correct to a developer reading the code but behave unexpectedly at runtime.
The most impactful example is the magic hash vulnerability. PHP converts strings that look like scientific notation (e.g., "0e12345") to floating-point numbers for comparison. Many MD5 and SHA1 hashes happen to start with 0e followed entirely by digits — these are called “magic hashes”. If a stored password hash starts with 0e, and an attacker supplies a password whose hash also starts with 0e, the comparison md5($input) == $stored_hash evaluates to true because both sides become the float 0.0. Known magic hash inputs for common hash functions are publicly documented and trivially attempted.
Beyond magic hashes, loose comparison enables:
"0" == falseand"" == false— empty string and zero compare equal tofalse"1" == true— non-zero strings compare equal totrue0 == "any_string_not_starting_with_a_digit"— in PHP 7, any string that does not start with a digit compares equal to0- Array vs scalar comparisons that produce unexpected results
What Gets Flagged
The rule matches .php files where a superglobal value is on either side of a == operator.
// FLAGGED: token comparison with loose equality — vulnerable to type juggling
if ($_GET['token'] == $expected_token) {
// grant access
}
// FLAGGED: password hash comparison with ==
if (md5($_POST['password']) == $stored_hash) {
// magic hash bypass possible
}
// FLAGGED: admin check with loose comparison
if ($_COOKIE['role'] == 1) {
$is_admin = true;
}
// FLAGGED: reversed form — same problem
if ($secret == $_REQUEST['key']) {
// proceed
}
Remediation
- Use
===(strict equality) everywhere you compare user input. Strict equality checks type and value without any coercion. This is the single most important change:
// SAFE: strict comparison prevents type juggling
if ($_GET['token'] === $expected_token) {
// only matches if type AND value are identical
}
- Use
hash_equals()for comparing secrets and tokens.hash_equals()performs a constant-time comparison that prevents timing attacks, and it also uses strict type checking internally:
// SAFE: constant-time, type-safe token comparison
if (hash_equals($expected_token, $_GET['token'] ?? '')) {
// grant access
}
- Never use MD5 or SHA1 alone for password storage. Use
password_hash()andpassword_verify(), which use bcrypt by default and are immune to both magic hash attacks and timing attacks:
// SAFE: proper password hashing and verification
// At registration:
$hash = password_hash($password, PASSWORD_BCRYPT);
// At login:
if (password_verify($_POST['password'], $stored_hash)) {
// authenticated
}
- Cast user input to the expected type before comparison. When you expect an integer, cast it first so that the comparison operates on integers regardless of operator:
// SAFE: explicit cast before comparison
$page = (int) ($_GET['page'] ?? 1);
if ($page === 1) { /* ... */ }
- Enable strict types at the top of every PHP file.
declare(strict_types=1)enforces strict type checking for function arguments and return values, and it changesswitchstatement comparison behavior to use strict equality for thecaseexpressions:
<?php
declare(strict_types=1);
// With strict_types, function signatures are enforced
function check_token(string $provided, string $expected): bool {
return hash_equals($expected, $provided);
}
- Review magic hash tables for your hash algorithm. If you have existing password hashes stored as unsalted MD5 or SHA1 strings in your database, any of them may be magic hashes. Migrate to
password_hash()by rehashing on next successful login.