Type Juggling
debt(d5/e3/b5/t9)
Closest to 'specialist tool catches it' (d5). The detection_hints list phpstan, psalm, and semgrep — all specialist static analysis tools — as able to catch == comparisons with mixed types and missing strict third argument in in_array(). Default linters don't flag this; it requires intentional static analysis configuration.
Closest to 'simple parameterised fix' (e3). The quick_fix is to replace == with === and add strict_types=1. This is a mechanical substitution, but it requires finding every loose comparison in security-sensitive paths across potentially multiple files — more than a single one-line patch but not a large-scale refactor. The pattern replacement is straightforward once identified.
Closest to 'persistent productivity tax' (b5). The term applies_to web, cli, and queue-worker contexts — the full PHP application surface. Every developer touching authentication, token validation, or type-sensitive comparisons must carry the mental overhead of always preferring === and auditing in_array() calls. It's a pervasive productivity tax but doesn't define the entire system architecture.
Closest to 'catastrophic trap' (t9). The misconception field explicitly states developers believe type juggling only produces edge-case oddities, when in fact '0e123' == '0e5678' is true in authentication logic — a silent, exploitable security bypass. The 'obvious' approach of using == for equality is always wrong in security contexts, and the failure mode is a complete authentication bypass rather than a minor bug.
Also Known As
TL;DR
Explanation
PHP's loose comparison operator (==) performs type coercion before comparing. "Magic hash" strings starting with 0e followed by digits are treated as scientific notation floats — so "0e1234" == "0e5678" evaluates to true because both equal 0.0. This can bypass hash comparison checks if the comparison uses == instead of ===. Similarly, 0 == "any_string_not_starting_with_digit" is true in older PHP. Always use strict comparison (===) when comparing security-sensitive values.
How It's Exploited
# With loose ==, the string '0' == 0 == false == null — unintended matches abound
Common Misconception
Why It Matters
Common Mistakes
- Using == to compare password hashes — magic hashes starting with 0e all equal each other.
- Comparing user input to boolean true with == — any non-empty string equals true loosely.
- Using in_array() without the strict third argument — in_array('0', [false]) returns true.
- JSON-decoded values changing type compared to expected type — use === always for security comparisons.
Avoid When
- Never use == for comparing passwords, tokens, or hashes — type juggling can make '0e123' == '0e456' true.
- Avoid loose comparisons with in_array() — always pass true as the third argument for strict mode.
When To Use
- Use strict comparison (===) for all security-sensitive checks — authentication, token comparison, type validation.
- Enable strict_types=1 to enforce type safety at the function call level.
Code Examples
// PHP loose comparison — 0 == 'admin' is TRUE in PHP < 8
if ($_POST['role'] == 0) { grantAccess(); }
// Magic hash: md5('240610708') === '0e462097431906509019562988736854'
// 0e... == 0e... is TRUE (scientific notation comparison)
if ($hash == $storedHash) { ... }
// Always use strict comparison
if ($_POST['role'] === 0) { grantAccess(); }
// For hash comparison, use hash_equals() (also constant-time)
if (hash_equals($storedHash, $userHash)) { ... }