# Type Juggling in PHP: Loose Comparisons as a Security Risk

> Understand PHP type juggling: how == compares, what PHP 8.0 changed and why tokens, hashes and roles must be compared strictly.

Source: https://phpsos.de/en/blog/type-juggling-php/  
As of: 25/10/2026  
Provider: PHP SOS, PHP emergency help and server security. Immediate help via WhatsApp: https://wa.me/491736494914?text=Hello%20PHP%20SOS%2C%20we%20need%20help%20with%20our%20PHP%20server.%20Company%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**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
<?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](https://phpsos.de/en/blog/sichere-tokens-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:

```bash
# 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](https://phpsos.de/en/php/typsystem/); how inputs are brought into the right type beforehand is shown in [Validating and Filtering Input in PHP](https://phpsos.de/en/blog/eingaben-validieren-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](https://phpsos.de/en/php2shell/php-8-1-und-aelter/).

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](https://phpsos.de/en/leistungen/sicherheitsaudit-penetrationstest/).


## 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

- [PHP manual: comparison operators](https://www.php.net/manual/de/language.operators.comparison.php)
- [PHP manual: match](https://www.php.net/manual/de/control-structures.match.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
