Engineering · Oct 7, 2026 · 7 min read

Type juggling: when loose comparison decides access

PHP's == and JavaScript's == coerce types before comparing - a numeric-looking string that evaluates to zero, or a '0e' magic hash that equals another '0e' hash, turns an authentication check into a bypass.

PHP's == operator and JavaScript's == operator coerce both sides to a common type before comparing. A login endpoint that compares a user-supplied token to a stored value with == can be bypassed by sending a numeric-looking string that PHP evaluates to the same integer as the token. A role check that tests if(role == 0) is true for the boolean false, the string '0', an empty string, and the integer 0 alike. None of these payloads look malformed. The confirming artifact is a boolean differential, not an error message.

01Type coercion: how == differs from ===

PHP's loose comparison table has been a source of bypasses since PHP 4. When one side is a number and the other is a string, PHP converts the string to a number before comparing. The string '0e1234' converts to float 0, so '0e1234' == '0e5678' is true - both look like scientific notation and both evaluate to zero. That is the class of bypass used against legacy PHP hash-comparison code: a stored MD5 or SHA1 hash that starts with '0e' compares equal to any other '0e...' candidate. JavaScript applies similar rules: 0 == false, null == undefined, '1' == 1. Each coercion is deterministic and documented, but none of them match what a developer intends when the code is a security gate.

PHP loose comparison (==) '0e1234' == '0e5678' ──► true # both coerce to float 0 (magic hash) 0 == 'admin' ──► true # non-numeric string coerces to 0 '0' == false ──► true # string '0' is falsy JavaScript (==) 0 == false ──► true # numeric 0 equals boolean false null == undefined──► true # null/undefined are loosely equal '1e1' == 10 ──► true # numeric string coerces to 10
Selected loose-comparison pairs in PHP and JavaScript. Each is deterministic and well-documented - none match developer intent when the check is a security gate.

02Confirming the bypass without gaining real access

The confirming probe uses a benign but characteristic payload that demonstrates the coercion without obtaining a privileged session. For a hash-comparison endpoint, the probe sends a '0e...' token candidate and observes whether the server grants a session. For a numeric-string bypass (0 == 'admin'), it sends a pure integer 0 where a non-numeric string is expected and watches whether the response code changes. The confirming artifact is the differential: a request that would be rejected with a correctly typed value succeeds with the coerced equivalent. Baseline and probe are the two halves of the evidence - no actual credential is derived and no privileged action is taken beyond demonstrating that the boundary is open.

type-juggling-probe.httphttp
# Baseline: random token - server rejects it cleanly
POST /api/login
{"username":"alice","token":"randomstring"}
HTTP/1.1 401 Unauthorized

# Probe: '0e' magic-hash pattern - if stored hash starts with '0e', PHP coerces both to 0
POST /api/login
{"username":"alice","token":"0e462097431906509019562988736854"}
HTTP/1.1 200 OK  Set-Cookie: session=...  # boundary failed - coercion confirmed

# Probe: numeric 0 where a role string is expected
GET /admin/panel
Authorization: Bearer 0
HTTP/1.1 200 OK  # if(role == 0) was true for string 'admin'
Proven - CriticalPHP type juggling - authentication bypass - /api/login token comparison
POST /api/login {"username":"alice","token":"0e462097431906509019562988736854"} ──► 200 OK Set-Cookie: session=eyJhbGci... (valid session for a forged token) baseline ("randomstring") ──► 401. Differential holds across 5 repeats. Nonce: 62615533.
Oracle: differential. Token value is a benign 0e-prefixed magic-hash candidate - no real credential derived, no privileged action taken. Severity critical: arbitrary authentication bypass with zero knowledge of a valid token value.

03Where type juggling surfaces across languages and gates

PHP is the primary surface because its comparison table is unusually permissive, but the class extends to any language with implicit coercion rules. JavaScript's == has documented coercion paths that bypass null-checks and role comparisons. Ruby's == delegates to the object's own method, which third-party types can override to produce surprising equality. Python's custom __eq__ implementations can return True for unexpected comparisons. Across all of them the common thread is not the language - it is a security gate that uses == on a value an attacker controls the type of.

LanguageCoercion patternBypass exampleConfirming probe
PHP0e magic hash'0e1234' == '0e5678' is trueSend a '0e...' token; observe session grant
PHPNumeric string vs number0 == 'admin' is trueSend integer 0 where string role expected; observe 200 vs 401
PHPType-juggling with arrays0 == array() differs across versionsSend array in scalar field; observe server error or unexpected pass
JavaScriptFalsy equality0 == false, '' == falseSend 0 or empty string where role level checked; observe permission change
JavaScriptnull/undefined equalitynull == undefined is trueSend null token; observe whether check short-circuits to success

04Severity follows the gate that breaks

Severity is set by the access control gate that the coercion bypasses, not by the mechanism. A type-juggling bypass on an authentication endpoint - a token comparison that accepts a magic-hash value - is critical, because it grants arbitrary session access to any caller who knows the coercion pattern. The same class on a role-level check that gates an admin panel is also critical. A coercion that affects only a cosmetic boolean - a feature flag, a display preference - is low. In every case the fix is the same: replace == with the strict equality operator (=== in PHP and JavaScript), enforce explicit type declarations on comparison operands, and reject non-string inputs at the API boundary before they reach any comparison that guards access. A linter rule that flags == in security-sensitive comparisons catches most of the surface at zero runtime cost.

0
records modified during the probe
1
operator change (== to ===) closes the class
5
repeats to rule out coincidence

Type juggling is easy to miss in code review precisely because the comparison looks correct - the equality operator is present, the operands are what they should be, and the logic reads naturally. The confirming probe is the only way to establish that the comparison fails in the face of an attacker who controls the type, not just the value. The arithmetic nonce 62615533 (7919x7907) appears in the probe log to confirm the test run identity without carrying any real credential.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →