CWE-597: Use of Wrong Operator in String Comparison - PHP
Overview
PHP's loose equality operator (==) performs type juggling before comparison, silently coercing operands to the same type. This creates security bypass conditions that do not exist in strictly typed languages. Some of the most surprising string-to-number cases were improved in PHP 8.0, but older PHP 7.x applications still have the legacy behavior and should be upgraded when possible. Notable examples:
- Legacy PHP <= 7.4:
"admin" == 0evaluates totruebecause the non-numeric string is coerced to integer0 - PHP 8.0+:
0 == "admin"evaluates tofalse, because non-strict number-to-non-numeric-string comparisons were made safer "0" == falseandnull == ""are still examples of why loose comparison is unsafe in security decisions"0e12345" == "0e67890"evaluates totruebecause both strings are treated as numeric zero
When authentication, authorization, or token validation code uses == for string comparison, an attacker who can control the type or format of an input value may bypass the check by supplying a value that type-juggles to match the expected one. Upgrading to PHP 8.0 or later removes the legacy non-numeric-string-to-number bypass, but strict comparison is still required for security-sensitive values.
Primary Defence: Use strict equality (===) for all security-sensitive string comparisons. Use password_verify() for passwords, and hash_equals() for token and MAC comparisons.
Common Vulnerable Patterns
Legacy PHP 7 Role Check with Loose Equality
<?php
// VULNERABLE on PHP <= 7.4 - type juggling with loose ==
function checkAdminRole($userRole): bool {
return $userRole == "admin"; // If $userRole is 0 (integer), this is TRUE in PHP 7
}
// Attack: if $userRole is read from a JSON field and arrives as integer 0:
// json_decode('{"role": 0}') -> $userRole = 0
// 0 == "admin" evaluates to true in PHP 7
checkAdminRole(0); // returns true!
Why this is vulnerable:
- In PHP 7.4 and earlier, comparing a string to an integer with
==casts the string to an integer first. Any non-numeric string casts to0, so"admin" == 0istrue. A JSON body whereroleis sent as0instead of"user"can match the admin check. - PHP 8.0 changed non-strict number-to-non-numeric-string comparison behavior, so this exact bypass no longer works on supported PHP 8+ runtimes. Treat a finding like this as both a code fix and an upgrade signal for legacy PHP applications.
Token Comparison with ==
<?php
// VULNERABLE - loose comparison of tokens
function validateCsrfToken(string $formToken, string $sessionToken): bool {
return $formToken == $sessionToken; // timing attack + potential type juggling
}
// Also vulnerable to timing attacks - execution time reveals information
// about how many leading characters match
Why this is vulnerable:
- String comparison with
==is not timing-safe: the function short-circuits as soon as it finds a mismatch. An attacker can measure response times to infer how many leading bytes of the token are correct, enabling a character-by-character brute-force attack.
Hash Comparison with ==
<?php
// VULNERABLE - comparing hashes with == allows type juggling bypass
$storedHash = getHashFromDatabase($username); // e.g., "0e123456789" (starts with 0e)
$inputHash = md5($_POST['password']); // e.g., "0e987654321"
if ($inputHash == $storedHash) { // VULNERABLE: "0e..." strings compare equal (scientific notation)
loginUser($username);
}
Why this is vulnerable:
- Strings matching
0e[0-9]+are treated as scientific notation (zero to some power, i.e., zero) in PHP's loose comparison. Finding an MD5 or SHA-1 input that produces a0e-prefixed hash allows bypassing authentication.
Secure Patterns
Strict Equality for Role/Permission Checks
<?php
declare(strict_types=1);
// SECURE: === checks both value AND type; no coercion
function checkRole(string $userRole, string $requiredRole): bool {
return $userRole === $requiredRole;
}
// SECURE: explicit type annotation prevents integer input from reaching this function
function isAdmin(string $role): bool {
return $role === 'admin';
}
Why this works:
===requires that both operands are the same type and the same value."admin" === 0isfalsebecause one is a string and the other is an integer.declare(strict_types=1)causes aTypeErrorif a caller passes a non-string tostring $role.
Constant-Time Token Comparison with hash_equals()
<?php
declare(strict_types=1);
// SECURE: hash_equals() is constant-time (not short-circuit) and type-safe
function validateCsrfToken(string $formToken, string $sessionToken): bool {
return hash_equals($sessionToken, $formToken);
}
function validateResetToken(string $submittedToken, string $storedToken): bool {
// Both arguments are strings; hash_equals handles timing safety
return hash_equals($storedToken, $submittedToken);
}
Why this works:
hash_equals()always compares the full string before returning, taking the same amount of time regardless of how many bytes match. This eliminates timing side-channels. It also requires both arguments to be strings, avoiding type juggling.
Password Verification with password_verify()
<?php
declare(strict_types=1);
// SECURE: password_verify is timing-safe and handles the stored salt
function loginUser(string $username, string $submittedPassword): bool {
$user = getUserFromDatabase($username);
if ($user === null) {
// Perform a dummy verify to prevent user enumeration via timing
password_verify($submittedPassword, '$2y$12$invalidhashinvalidhashinvalidhash');
return false;
}
return password_verify($submittedPassword, $user->passwordHash);
}
Why this works:
password_verify()is designed for password comparison: it is timing-safe, extracts the salt from the stored hash automatically, and handles algorithm differences. Never compare password hashes with==,===, orhash_equals()- onlypassword_verify()is correct for passwords.
Remediation Steps
Locate the Finding
- Source: Any string value used in an authentication, authorization, or token validation comparison
- Sink:
==operator with string operands in security-sensitive code
Apply the Fix
- PRIORITY 1: Replace
==with===in all role checks, token comparisons, and string-based authorization decisions - PRIORITY 2: Replace direct token comparison with
hash_equals($expected, $submitted)for CSRF tokens and reset links - PRIORITY 3: Replace any direct hash comparison for passwords with
password_verify()
Verify the Fix
- Test
checkRole(0, 'admin')- it should returnfalse - Test CSRF token comparison with a token of value
0- it should fail even if the session token is"0" - Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: == in authentication/authorization functions, != comparisons with security tokens, any comparison where one operand could be numeric
Testing
- Normal input: verify valid roles, session tokens, CSRF tokens, and passwords still pass their intended checks.
- Boundary input: test empty strings,
"0",0,false,null, and numeric-looking strings such as"0e12345". - Malicious input: replay type-juggling payloads against authorization and token comparisons; confirm strict comparison or
hash_equals()rejects them.