# Type juggling in PHP: losse vergelijkingen als beveiligingsrisico

> PHP type juggling begrijpen: hoe == vergelijkt, wat PHP 8.0 heeft veranderd en waarom tokens, hashes en rollen strikt moeten worden vergeleken.

Bron: https://phpsos.de/nl/blog/type-juggling-php/  
Stand: 25-10-2026  
Aanbieder: PHP SOS, PHP-noodhulp en serverbeveiliging. Directe hulp via WhatsApp: https://wa.me/491736494914?text=Hallo%20PHP%20SOS%2C%20wij%20hebben%20hulp%20nodig%20met%20onze%20PHP-server.%20Bedrijf%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**Kort gezegd:** PHP type juggling is de automatische typeomzetting die bij de losse vergelijking met `==` over het resultaat beslist. Sinds PHP 8.0 zijn de regels strenger, maar numerieke tekenreeksen worden nog steeds als getallen vergeleken en `true`, `false` en `null` volgen eigen regels. Voor beveiligingsrelevante controles geldt daarom: `===`, `in_array(..., true)`, `match` in plaats van `switch`, `hash_equals()` voor tokens en `password_verify()` voor wachtwoorden.

## Een vergelijking die waar is, terwijl ze dat niet zou moeten zijn

Veel beveiligingslekken in PHP-applicaties zitten niet in ingewikkelde functies, maar in één enkel teken: een `==` waar een `===` had moeten staan. De code ziet er correct uit, tests met normale invoer slagen en toch laat de controle onder bepaalde omstandigheden iets door wat ze had moeten stoppen.

De oorzaak is een eigenschap van PHP die in het dagelijks gebruik praktisch is: waarden worden indien nodig in een ander type omgezet. Een tekenreeks uit een formulier kan zo zonder omweg worden vergeleken met een getal uit de database. Bij logins, tokens, rollen of statusvelden kan juist die ruimhartigheid gevaarlijk worden.

## Hoe PHP met == vergelijkt

De PHP-handleiding beschrijft de regels voor losse vergelijkingen uitvoerig. Voor de beveiliging zijn vooral deze gevallen belangrijk:

| Vergelijking | PHP 7 | PHP 8 | Reden |
|---|---|---|---|
| `0 == "a"` | waar | onwaar | sinds PHP 8.0 als tekenreeksen vergeleken |
| `"1" == "01"` | waar | waar | beide numeriek, vergelijking als getal |
| `"10" == "1e1"` | waar | waar | exponentiële notatie is numeriek |
| `100 == "1e2"` | waar | waar | getal tegen numerieke tekenreeks |
| `null == false` | waar | waar | vergelijking als waarheidswaarde |
| `"abc" == true` | waar | waar | niet-lege tekenreeks geldt als waar |

De wijziging in PHP 8.0 heeft een hele klasse fouten ontschaard: vroeger werd een niet-numerieke tekenreeks bij de vergelijking met een getal `0`. Dat gold ook voor `switch`-instructies. De overige regels van de tabel laten echter zien dat losse vergelijkingen nog steeds verrassingen bevatten.

Ook de operatoren `<`, `>` en de vergelijkingsoperator `<=>` volgen deze regels. Bij sorteringen is dat meestal onschuldig, maar bij controles zoals "bedrag kleiner dan limiet" met waarden uit het verzoek moet het type van tevoren vaststaan.

Bijzonder verraderlijk is de regel met de exponentiële notatie. Hashwaarden en tokens bestaan vaak uit cijfers en letters. Ziet zo'n waarde er toevallig uit als een getal in exponentiële notatie, dan vergelijkt `==` twee verschillende hashes mogelijk als gelijk. Dat is de achtergrond van veel meldingen over het omzeilen van logins in oudere PHP-applicaties.

## PHP type juggling in typische beveiligingscontroles

Losse vergelijkingen verstoppen zich op meer plekken dan alleen in de operator `==`:

1. **`in_array()` en `array_search()`** vergelijken zonder derde parameter los. Een lijst met toegestane rollen of ID's wordt daardoor zachter dan gedacht.
2. **`switch`** vergelijkt los. De sinds PHP 8.0 beschikbare expressie `match` vergelijkt volgens de handleiding daarentegen typeveilig met `===` en geeft een fout als geen enkele tak past.
3. **JSON-invoer** levert echte typen. Een veld dat u als tekenreeks verwacht, kan als `true` of als getal binnenkomen en met `==` onverwacht overeenkomen.
4. **`strcmp()` voor geheimen** is ongeschikt, omdat de looptijd van de inhoud afhangt en foutgevallen gemakkelijk verkeerd worden geëvalueerd.
5. **Controle van retourwaarden**: functies zoals `strpos()` leveren `0` voor een treffer aan het begin en `false` voor geen treffer. `if (!strpos(...))` verwart beide.

## Veilige vergelijkingen: de tegenstelling

De volgende voorbeelden tonen telkens de onveilige en de veilige variant:

```php
<?php
declare(strict_types=1);

// Rol controleren
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen);          // onveilig: los
in_array($rolle, $erlaubteRollen, true);    // veilig: strikt

// Status beoordelen
$aktion = match ($status) {                 // strikt, fout bij onbekende waarde
    'offen'      => 'bearbeiten',
    'erledigt'   => 'archivieren',
};

// Token uit link controleren
if ($tokenAusLink == $gespeichertesToken) { /* onveilig */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* veilig */ }

// Wachtwoord controleren
if (md5($eingabe) == $hash) { /* onveilig: zwakke methode en losse vergelijking */ }
if (password_verify($eingabe, $hash)) { /* veilig */ }

// Deelreeks zoeken
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
```

`hash_equals()` verwacht volgens de handleiding het bekende geheim als eerste en de waarde van de gebruiker als tweede parameter en vergelijkt in constante tijd. Hoe u tokens überhaupt veilig genereert, beschrijft [Veilige willekeurige getallen en tokens in PHP](https://phpsos.de/nl/blog/sichere-tokens-php/).

> **Belangrijk:**
> `declare(strict_types=1)` maakt vergelijkingen niet strikt. De instructie betreft alleen de typeomzetting bij functieaanroepen en retourwaarden. Losse vergelijkingen moet u zelf in de code vervangen.

Strikte vergelijkingen hebben een keerzijde: ze mislukken als de typen niet bij elkaar passen. `'42' === 42` is onwaar. Waarden uit formulieren en URL's komen in PHP als tekenreeksen binnen, waarden uit de database afhankelijk van driver en instelling als tekenreeksen of getallen. Wie overal `===` invoert zonder de typen gelijk te trekken, veroorzaakt fouten die in het bedrijf opvallen als afwijzing van legitieme gebruikers.

De oplossing is een duidelijke grens: invoer wordt bij ontvangst gevalideerd en in het juiste type gebracht, bijvoorbeeld met `filter_var($wert, FILTER_VALIDATE_INT)`, en daarna in de code alleen nog met typedeclaraties en strikte vergelijkingen behandeld. Typedeclaraties bij parameters, retourwaarden en eigenschappen helpen daarbij, omdat verkeerde typen dan vroeg met een foutmelding opvallen in plaats van stilletjes in een vergelijking te verdwijnen.

## Bestaande code systematisch controleren

In gegroeide projecten staan losse vergelijkingen bij honderden. Niet elke is een probleem. Concentreer u op plekken waar een beslissing over toegang valt:

```bash
# Losse vergelijkingen in regels met beveiligingsrelevante termen
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array zonder strikte vergelijking
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
```

Hulpmiddelen voor statische analyse zoals PHPStan of Psalm kunnen losse vergelijkingen op verzoek blijvend melden. Zo komen nieuwe gevallen niet eens in de hoofdtak terecht. Basisprincipes over typen beschrijft [Het typesysteem van PHP](https://phpsos.de/nl/php/typsystem/); hoe invoer van tevoren in het juiste type wordt gebracht, toont [Invoer in PHP valideren en filteren](https://phpsos.de/nl/blog/eingaben-validieren-php/).

## Wat dit betekent voor oude systemen

Applicaties die nog op PHP 7 draaien, vallen onder de oude, lossere vergelijkingsregels. Daar komt een tweede probleem bij: voor PHP 8.1 en ouder zijn er van het PHP-project geen officiële beveiligingsupdates meer, ook niet tegen PHP2Shell. De situatie beschrijft [PHP 8.1 en ouder: wat nu geldt](https://phpsos.de/nl/php2shell/php-8-1-und-aelter/).

Bij een migratie naar PHP 8 veranderen sommige vergelijkingsresultaten. Dat is meestal een verbetering, maar kan code breken die op het oude gedrag vertrouwde. Plan daarom tests in die gericht vergelijkingen met getallen, lege waarden en `null` dekken.

## Hoe helpt PHP SOS concreet?

- Wij doorzoeken uw code op losse vergelijkingen op plekken die over aanmelding, rechten of betalingen beslissen.
- Wij vervangen ze door strikte vergelijkingen, `match`-expressies en passende controlefuncties.
- Wij richten statische analyse zo in dat nieuwe losse vergelijkingen in de build opvallen.
- Wij begeleiden migraties naar actuele PHP-versies met tests voor gewijzigde vergelijkingsregels.

Een controle van uw software beschrijft [beveiligingsaudit en penetratietest](https://phpsos.de/nl/leistungen/sicherheitsaudit-penetrationstest/).


## Veelgestelde vragen

**Wat is type juggling in PHP?**  
PHP zet waarden van verschillende typen bij sommige bewerkingen automatisch om, bijvoorbeeld een tekenreeks in een getal. Bij de losse vergelijking met == beslist deze omzetting over het resultaat. Dat is handig, maar kan leiden tot vergelijkingen die waar zijn, hoewel de waarden er niet gelijk uitzien.

**Heeft PHP 8 het probleem opgelost?**  
Gedeeltelijk. Sinds PHP 8.0 wordt een getal met een niet-numerieke tekenreeks als tekenreeks vergeleken, 0 == 'a' is dus niet meer waar. Twee numerieke tekenreeksen worden echter nog steeds als getallen vergeleken en vergelijkingen met true, false en null volgen eigen regels.

**Helpt declare(strict_types=1) tegen losse vergelijkingen?**  
Nee. strict_types bepaalt of scalaire typen bij functieaanroepen en retourwaarden automatisch worden omgezet. Op de vergelijkingsoperatoren == en != heeft het geen invloed.

**Hoe vergelijk ik wachtwoorden en tokens correct?**  
Wachtwoorden controleert u met password_verify, tokens en handtekeningen met hash_equals. Beide vergelijken exact en, bij hash_equals, in constante tijd. Een vergelijking met == of strcmp is daarvoor ongeschikt.

## Bronnen

- [PHP-handleiding: vergelijkingsoperatoren](https://www.php.net/manual/de/language.operators.comparison.php)
- [PHP-handleiding: match](https://www.php.net/manual/de/control-structures.match.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
