Current situation: PHP2Shell. Two critical PHP vulnerabilities (CVSS 9.2) allow malicious code without login. Patches since 21 October 2026.What to do now →

Type Juggling in PHP: Loose Comparisons as a Security Risk

Why loose comparisons with == in PHP can lead to security vulnerabilities, what changed with PHP 8.0 and how to write robust comparisons in logins, tokens and permissions.

Last updated 6 min readSecure development

In short: PHP type juggling is the automatic type conversion that decides the result of a loose comparison with ==. Since PHP 8.0 the rules are stricter, but numeric strings are still compared as numbers, and true, false and null follow their own rules. For security-relevant checks, the following therefore applies: ===, in_array(..., true), match instead of switch, hash_equals() for tokens and password_verify() for passwords.

A Comparison That Is True Although It Should Not Be

Many security vulnerabilities in PHP applications are not hidden in complicated functions but in a single character: a == where a === should have been. The code looks correct, tests with normal input pass, and yet under certain conditions the check lets something through that it should have stopped.

The cause is a property of PHP that is practical in everyday use: values are converted to another type when needed. A string from a form can thus be compared with a number from the database without a detour. For logins, tokens, roles or status fields, exactly this generosity can become dangerous.

How PHP Compares with ==

The PHP manual describes the rules for loose comparisons in detail. For security, these cases are particularly important:

Comparison PHP 7 PHP 8 Reason
0 == "a" true false compared as strings since PHP 8.0
"1" == "01" true true both numeric, compared as numbers
"10" == "1e1" true true exponential notation is numeric
100 == "1e2" true true number against numeric string
null == false true true compared as boolean values
"abc" == true true true non-empty string counts as true

The change in PHP 8.0 defused a whole class of errors: previously, a non-numeric string became 0 when compared with a number. This also affected switch statements. The remaining rows of the table show, however, that loose comparisons still hold surprises.

The operators <, > and the spaceship operator <=> also follow these rules. For sorting this is usually harmless; for checks such as "amount less than limit" with values from the request, the type should be settled beforehand.

The row with exponential notation is especially tricky. Hash values and tokens often consist of digits and letters. If such a value happens to look like a number in exponential notation, == may compare two different hashes as equal. That is the background to many reports of login bypasses in older PHP applications.

PHP Type Juggling in Typical Security Checks

Loose comparisons hide in more places than just the == operator:

  1. in_array() and array_search() compare loosely without a third parameter. An allowlist of roles or IDs thereby becomes softer than intended.
  2. switch compares loosely. The match expression, available since PHP 8.0, by contrast compares in a type-safe way with === according to the manual and throws an error if no branch matches.
  3. JSON input delivers real types. A field you expect as a string can arrive as true or as a number and match surprisingly with ==.
  4. strcmp() for secrets is unsuitable because the runtime depends on the content and error cases are easily evaluated wrongly.
  5. Checking return values: functions such as strpos() return 0 for a match at the beginning and false for no match. if (!strpos(...)) confuses the two.

Secure Comparisons: Side by Side

The following examples each show the insecure and the secure variant:

<?php
declare(strict_types=1);

// Check role
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen);          // insecure: loose
in_array($rolle, $erlaubteRollen, true);    // secure: strict

// Evaluate status
$aktion = match ($status) {                 // strict, error on unknown value
    'offen'      => 'bearbeiten',
    'erledigt'   => 'archivieren',
};

// Check token from link
if ($tokenAusLink == $gespeichertesToken) { /* insecure */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* secure */ }

// Check password
if (md5($eingabe) == $hash) { /* insecure: weak algorithm and loose comparison */ }
if (password_verify($eingabe, $hash)) { /* secure */ }

// Search for a substring
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }

According to the manual, hash_equals() expects the known secret as the first and the user's value as the second parameter and compares in constant time. How to generate tokens securely in the first place is described in Secure Random Numbers and Tokens in PHP.

Important

declare(strict_types=1) does not make comparisons strict. The statement only affects type conversion in function calls and return values. You have to replace loose comparisons in the code yourself.

Strict comparisons have a downside: they fail when the types do not match. '42' === 42 is false. Values from forms and URLs arrive in PHP as strings, values from the database as strings or numbers depending on the driver and settings. Anyone who introduces === everywhere without aligning the types creates errors that show up in operation as the rejection of legitimate users.

The solution is a clear boundary: inputs are validated on receipt and converted to the right type, for example with filter_var($wert, FILTER_VALIDATE_INT), and afterwards handled in the code only with type declarations and strict comparisons. Type declarations on parameters, return values and properties help here because wrong types then show up early with an error message instead of silently disappearing in a comparison.

Systematically Reviewing Existing Code

In grown projects, loose comparisons are found by the hundreds. Not every one is a problem. Concentrate on places where a decision about access is made:

# Loose comparisons in lines with security-relevant terms
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array without strict comparison
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"

Static analysis tools such as PHPStan or Psalm can, if desired, report loose comparisons permanently. That way, new cases never reach the main branch. Basics on types are described in The PHP Type System; how inputs are brought into the right type beforehand is shown in Validating and Filtering Input in PHP.

What This Means for Legacy Systems

Applications that still run on PHP 7 are affected by the old, laxer comparison rules. A second problem comes on top: for PHP 8.1 and older, the PHP project no longer provides official security updates, not even against PHP2Shell. The situation is described in PHP 8.1 and Older: What Applies Now.

With a migration to PHP 8, some comparison results change. This is usually an improvement but can break code that relied on the old behavior. So plan tests that specifically cover comparisons with numbers, empty values and null.

How does PHP SOS help?

  • We search your code for loose comparisons in places that decide on login, permissions or payments.
  • We replace them with strict comparisons, match expressions and suitable check functions.
  • We set up static analysis so that new loose comparisons are flagged in the build.
  • We accompany migrations to current PHP versions with tests for changed comparison rules.

A review of your software is described by Security Audit and Penetration Test.

Frequently asked questions

What is type juggling in PHP?

For some operations, PHP automatically converts values of different types, for example a string into a number. In a loose comparison with ==, this conversion decides the result. That is convenient, but it can lead to comparisons that are true even though the values do not look equal.

Did PHP 8 solve the problem?

Partly. Since PHP 8.0, a number is compared with a non-numeric string as a string, so 0 == 'a' is no longer true. Two numeric strings are still compared as numbers, though, and comparisons with true, false and null follow their own rules.

Does declare(strict_types=1) help against loose comparisons?

No. strict_types controls whether scalar types are converted automatically in function calls and return values. It has no influence on the comparison operators == and !=.

How do I compare passwords and tokens correctly?

You check passwords with password_verify and tokens and signatures with hash_equals. Both compare exactly and, in the case of hash_equals, in constant time. A comparison with == or strcmp is unsuitable for this.

Sources

Immediate contact

Every hour counts. Write to us now.

Briefly describe your system, PHP version and observation. We will get back to you, clarify the situation and work remotely until the incident is closed and documented.

Immediate help