A penetration tester sends {"password": true} to a PHP login endpoint. The server returns a valid session token. The stored credential is a 60-character bcrypt hash. No brute force, no credential leak. A wrong type in the comparison is sufficient.
Type juggling converts a strict value comparison into a reachable bypass. PHP == coerces type before comparing. JavaScript == does the same. The REST API surface is more dangerous than the PHP form surface. JSON body parsers preserve types: true reaches the server as a boolean, not the string "true". A WAF sees valid JSON and fires no alerts.
How == and === Differ: Why It Matters in Auth Code
The == operator in PHP and JavaScript performs type coercion before comparing values. The === operator checks type and value without coercion. That one-character difference determines whether an authentication handler accepts or rejects unexpected types.
In PHP 7.x, 0 == "a" returns true. The string "a" coerces to integer 0 in numeric context, and 0 == 0 is true. In JavaScript, 0 == "" is true, null == undefined is true, and [] == false is true. The coercion table is implicit in both languages: most developers learn the rules by accident, not from the specification. Auth handlers that compare user-supplied input against stored values using == accept type-coerced bypass payloads.
The most dangerous PHP pair for auth is "0e[digits]" == "0e[other digits]". Both strings represent scientific exponents and coerce to the same numeric value: zero. This is not a runtime bug. It is specified behavior. In JavaScript, "0e1" == "0e2" does not trigger this coercion. JS does not auto-cast numeric strings to floats. The JS vector is different: true == 1 and "" == false are the most common auth code traps.
PHP Magic Hashes: MD5 Strings That Compare Equal Under ==
Some inputs produce MD5 and SHA1 hashes in the 0e format followed only by digits. PHP interprets these strings as scientific notation: both evaluate to zero. Two hashes in this format compare equal under ==, regardless of which password generated them.
MD5 of 240610708 is 0e462097431906509019562988736854. MD5 of QNKCDZO is 0e830400451993494058024219903391. SHA1 of aaroZmOk is 0e66507019969427134894567494305185566735. Under ==, any combination of these strings evaluates as 0 == 0. The PayloadsAllTheThings repository maintains a list of known inputs producing magic hashes for MD5, SHA1, and SHA256.
CVE-2022-47034 documents exactly this pattern: playSMS 1.4.5's auth/fn.php compared MD5 hashes with ==, allowing any magic-hash input to authenticate as any user. CVE-2025-47776 repeats the pattern in MantisBT 2.27.1 (CVSS 8.8): authentication_api.php used == for hash comparison on the REST endpoint, patched November 2025.
PHP 8.0 changed 0 == 'non-numeric-string' to false. PHP 7.x deployments remain vulnerable. Magic 0e hashes still collide with each other in PHP 8 when both sides are 0e strings, because PHP converts both to the same float value.
JSON Body Injection: Sending true Where a String Is Expected
This is the REST API-specific surface. A JSON body like {"password": true} delivers a boolean to the PHP or Node.js server. In PHP, $password == $storedHash where $password = true and $storedHash is a non-empty string evaluates to true. PHP's == with a boolean operand converts both sides to bool. Any non-empty string is truthy, so true == $storedHash evaluates as true == true — true regardless of the hash content.
CVE-2023-6875 documents the most exploited case. The POST SMTP Mailer plugin for WordPress (300k+ installations) exposed a REST endpoint that accepted a blank authentication key. The stored authentication key was null for users who had never set it. Comparing null == null — both the stored value and the absent request value — passed the check without any valid credential. CVSS 9.8. Wordfence published the technical writeup showing the exact vulnerable endpoint code.
CVE-2021-26600 (ImpressCMS <= 1.4.2) documents the same pattern in autologin.php. The operator != instead of !== made the auto-login token bypassable without knowing the password. Two patch iterations were required; version 1.4.3 partially fixed it and 1.4.4 closed the bug.
WAFs do not detect this attack. The payload {"password": true} is structurally valid JSON with the correct Content-Type: application/json. It contains no SQL injection, no XSS, no unusual encoding. It passes schema validation that only checks for the presence of the password field, not its type. Detection requires semantic inspection of the body, not just syntactic validation.
JavaScript: JSON.parse Preserves Types That == Coerces
JSON.parse('{"password": true}') returns {password: true} with typeof password === 'boolean'. Node.js handlers expose a different coercion path. When a user lookup returns null and the code does null == req.body.token, any request where token is null in the JSON body passes the check — both sides are null, null == null is true. The fix is === and an explicit null check before comparison.
Auth0 published a 2015 analysis of JWT libraries that accepted alg: none. A subset of those libraries compared the algorithm field without pinning the expected value and without case normalization. Variants like None, NONE, and nOnE bypassed the check. The same logic applies to password comparisons. The difference between === and == in an auth handler separates a secure server from a bypassable one.
H1 #86022 (Phabricator) documents validateCSRFToken() bypassable via loose comparison. H1 #202774 (ExpressionEngine) shows type juggling opening access to unserialize() with user-supplied input, leading to SQL injection. The pattern repeats: a comparison that accepts wrong types opens the next layer of the attack stack.
Five CVEs, Same Root Cause
| CVE | Product | CVSS | Mechanism |
|---|---|---|---|
| CVE-2022-47034 | playSMS 1.4.5 | n/a | MD5 + == in auth handler |
| CVE-2021-26600 | ImpressCMS <= 1.4.2 | n/a |
!= in token comparison |
| CVE-2023-6875 | POST SMTP Mailer | 9.8 | Blank key coerced truthy |
| CVE-2025-47776 | MantisBT <= 2.27.1 | 8.8 |
== hash comparison, REST API |
| CVE-2026-22205 | SPIP < 4.4.10 | 8.7 | Type juggling in auth logic |
ImpressCMS required two patch iterations. MantisBT carried the vulnerability into November 2025. SPIP was disclosed in 2026 with CVSS 8.7, allowing unauthenticated access to protected information. No general-purpose linter detects == as a problem in authentication context. The operator is valid elsewhere, and static analysis tools do not distinguish the two uses without semantic annotation.
The pattern is consistent: a developer uses == by habit in an auth handler and never gets feedback that the operator is wrong. Code review rarely catches it; automated tests rarely send wrong types.
Fix: Strict Equality and Type Validation Before Comparison
Three controls eliminate this vector:
- Use
===(PHP, JS) or type-safe comparison libraries for all authentication comparisons. Never==. - Validate JSON body field types at the schema layer before reaching business logic. Reject
{"password": true}with a400before any comparison runs. - Use
hash_equals()in PHP for timing-safe comparison. It also enforces string type.
In JavaScript/Node.js, validate with Zod (z.string().min(1)) before any comparison. A typeof check alone does not protect against object subclasses. JSON Schema with "type": "string" on the password field rejects booleans and numbers at the parsing layer, before any handler runs.
In Python, assert isinstance(value, str) before comparison is the minimum control. Use secrets.compare_digest() for timing-safe comparison. The complete defense stack is: schema validation (reject wrong type), strict equality (=== or hash_equals), timing-safe comparison (prevent timing oracle).
The MAGO Intel tool (intel.mago.team) tests API auth endpoints with type-mismatched JSON bodies. It sends booleans, null, integer 0, and empty string where password fields are expected.
The JSON body injection vector is invisible to most WAFs and to most developers trained on PHP web form exploits. The fix is a single character change: === instead of ==, and a schema type check before the comparison runs.