Kurz gesagt: PHP Type Juggling bezeichnet die automatische Typumwandlung, die beim losen Vergleich mit == über das Ergebnis entscheidet. Seit PHP 8.0 sind die Regeln strenger, aber numerische Zeichenketten werden weiter als Zahlen verglichen, und true, false und null folgen eigenen Regeln. Für sicherheitsrelevante Prüfungen gilt deshalb: ===, in_array(..., true), match statt switch, hash_equals() für Tokens und password_verify() für Passwörter.
Ein Vergleich, der wahr ist, obwohl er es nicht sein sollte
Viele Sicherheitslücken in PHP-Anwendungen stecken nicht in komplizierten Funktionen, sondern in einem einzigen Zeichen: einem ==, wo ein === hingehört hätte. Der Code sieht korrekt aus, Tests mit normalen Eingaben laufen durch, und trotzdem lässt die Prüfung unter bestimmten Bedingungen etwas passieren, das sie stoppen sollte.
Die Ursache ist eine Eigenschaft von PHP, die im Alltag praktisch ist: Werte werden bei Bedarf in einen anderen Typ umgewandelt. Eine Zeichenkette aus einem Formular kann so ohne Umweg mit einer Zahl aus der Datenbank verglichen werden. Bei Logins, Tokens, Rollen oder Statusfeldern kann genau diese Großzügigkeit gefährlich werden.
Wie PHP mit == vergleicht
Das PHP-Handbuch beschreibt die Regeln für lose Vergleiche ausführlich. Für die Sicherheit sind vor allem diese Fälle wichtig:
| Vergleich | PHP 7 | PHP 8 | Grund |
|---|---|---|---|
0 == "a" |
wahr | falsch | seit PHP 8.0 als Zeichenketten verglichen |
"1" == "01" |
wahr | wahr | beide numerisch, Vergleich als Zahl |
"10" == "1e1" |
wahr | wahr | Exponentialschreibweise ist numerisch |
100 == "1e2" |
wahr | wahr | Zahl gegen numerische Zeichenkette |
null == false |
wahr | wahr | Vergleich als Wahrheitswert |
"abc" == true |
wahr | wahr | nicht leere Zeichenkette gilt als wahr |
Die Änderung in PHP 8.0 hat eine ganze Klasse von Fehlern entschärft: Früher wurde eine nicht numerische Zeichenkette beim Vergleich mit einer Zahl zu 0. Das betraf auch switch-Anweisungen. Die übrigen Zeilen der Tabelle zeigen aber, dass lose Vergleiche weiter Überraschungen bergen.
Auch die Operatoren <, > und der Vergleichsoperator <=> folgen diesen Regeln. Bei Sortierungen ist das meist harmlos, bei Prüfungen wie „Betrag kleiner als Limit“ mit Werten aus der Anfrage sollte der Typ vorher feststehen.
Besonders tückisch ist die Zeile mit der Exponentialschreibweise. Hash-Werte und Tokens bestehen oft aus Ziffern und Buchstaben. Sieht ein solcher Wert zufällig aus wie eine Zahl in Exponentialschreibweise, vergleicht == zwei verschiedene Hashes womöglich als gleich. Das ist der Hintergrund vieler Berichte über Login-Umgehungen in älteren PHP-Anwendungen.
PHP Type Juggling in typischen Sicherheitsprüfungen
Lose Vergleiche verstecken sich an mehr Stellen als nur im Operator ==:
in_array()undarray_search()vergleichen ohne dritten Parameter lose. Eine Erlaubnisliste mit Rollen oder IDs wird dadurch weicher als gedacht.switchvergleicht lose. Der seit PHP 8.0 verfügbare Ausdruckmatchvergleicht laut Handbuch dagegen typsicher mit===und wirft einen Fehler, wenn kein Zweig passt.- JSON-Eingaben liefern echte Typen. Ein Feld, das Sie als Zeichenkette erwarten, kann als
trueoder als Zahl ankommen und mit==überraschend übereinstimmen. strcmp()für Geheimnisse ist ungeeignet, weil die Laufzeit vom Inhalt abhängt und Fehlerfälle leicht falsch ausgewertet werden.- Prüfung von Rückgabewerten: Funktionen wie
strpos()liefern0für einen Treffer am Anfang undfalsefür keinen Treffer.if (!strpos(...))verwechselt beides.
Sichere Vergleiche: die Gegenüberstellung
Die folgenden Beispiele zeigen jeweils die unsichere und die sichere Variante:
<?php
declare(strict_types=1);
// Rolle prüfen
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // unsicher: lose
in_array($rolle, $erlaubteRollen, true); // sicher: strikt
// Status auswerten
$aktion = match ($status) { // strikt, Fehler bei unbekanntem Wert
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Token aus Link prüfen
if ($tokenAusLink == $gespeichertesToken) { /* unsicher */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* sicher */ }
// Passwort prüfen
if (md5($eingabe) == $hash) { /* unsicher: schwaches Verfahren und loser Vergleich */ }
if (password_verify($eingabe, $hash)) { /* sicher */ }
// Teilstring suchen
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() erwartet laut Handbuch das bekannte Geheimnis als ersten und den Wert des Nutzers als zweiten Parameter und vergleicht in konstanter Zeit. Wie Sie Tokens überhaupt sicher erzeugen, beschreibt Sichere Zufallszahlen und Tokens in PHP.
declare(strict_types=1) macht Vergleiche nicht strikt. Die Anweisung betrifft nur die Typumwandlung bei Funktionsaufrufen und Rückgabewerten. Lose Vergleiche müssen Sie im Code selbst ersetzen.
Strikte Vergleiche haben eine Kehrseite: Sie scheitern, wenn die Typen nicht zusammenpassen. '42' === 42 ist falsch. Werte aus Formularen und URLs kommen in PHP als Zeichenketten an, Werte aus der Datenbank je nach Treiber und Einstellung als Zeichenketten oder Zahlen. Wer überall === einführt, ohne die Typen anzugleichen, erzeugt Fehler, die im Betrieb als Ablehnung berechtigter Nutzer auffallen.
Die Lösung ist eine klare Grenze: Eingaben werden beim Empfang validiert und in den richtigen Typ gebracht, etwa mit filter_var($wert, FILTER_VALIDATE_INT), und danach im Code nur noch mit Typdeklarationen und strikten Vergleichen behandelt. Typdeklarationen an Parametern, Rückgaben und Eigenschaften helfen dabei, weil falsche Typen dann früh mit einer Fehlermeldung auffallen, statt still in einem Vergleich zu verschwinden.
Bestandscode systematisch prüfen
In gewachsenen Projekten finden sich lose Vergleiche zu Hunderten. Nicht jeder ist ein Problem. Konzentrieren Sie sich auf Stellen, an denen eine Entscheidung über Zugriff fällt:
# Lose Vergleiche in Zeilen mit sicherheitsrelevanten Begriffen
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array ohne strikten Vergleich
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Werkzeuge zur statischen Analyse wie PHPStan oder Psalm können lose Vergleiche auf Wunsch dauerhaft melden. So gelangen neue Fälle gar nicht erst in den Hauptzweig. Grundlagen zu Typen beschreibt Das Typsystem von PHP; wie Eingaben vorher in den richtigen Typ gebracht werden, zeigt Eingaben in PHP validieren und filtern.
Was das für Altsysteme bedeutet
Anwendungen, die noch auf PHP 7 laufen, sind von den alten, laxeren Vergleichsregeln betroffen. Dazu kommt ein zweites Problem: Für PHP 8.1 und älter gibt es vom PHP-Projekt keine offiziellen Sicherheitsupdates mehr, auch nicht gegen PHP2Shell. Die Lage beschreibt PHP 8.1 und älter: Was jetzt gilt.
Bei einer Migration auf PHP 8 ändern sich manche Vergleichsergebnisse. Das ist meist eine Verbesserung, kann aber Code brechen, der sich auf das alte Verhalten verlassen hat. Planen Sie deshalb Tests ein, die gezielt Vergleiche mit Zahlen, leeren Werten und null abdecken.
Wie hilft PHP SOS konkret?
- Wir durchsuchen Ihren Code nach losen Vergleichen an Stellen, die über Anmeldung, Rechte oder Zahlungen entscheiden.
- Wir ersetzen sie durch strikte Vergleiche,
match-Ausdrücke und passende Prüffunktionen. - Wir richten statische Analyse so ein, dass neue lose Vergleiche im Build auffallen.
- Wir begleiten Migrationen auf aktuelle PHP-Versionen mit Tests für geänderte Vergleichsregeln.
Eine Prüfung Ihrer Software beschreibt Sicherheitsaudit und Penetrationstest.
Häufige Fragen
Was ist Type Juggling in PHP?
PHP wandelt Werte unterschiedlicher Typen bei manchen Operationen automatisch um, etwa eine Zeichenkette in eine Zahl. Beim losen Vergleich mit == entscheidet diese Umwandlung über das Ergebnis. Das ist bequem, kann aber zu Vergleichen führen, die wahr sind, obwohl die Werte nicht gleich aussehen.
Hat PHP 8 das Problem gelöst?
Teilweise. Seit PHP 8.0 wird eine Zahl mit einer nicht numerischen Zeichenkette als Zeichenkette verglichen, 0 == 'a' ist also nicht mehr wahr. Zwei numerische Zeichenketten werden aber weiterhin als Zahlen verglichen, und Vergleiche mit true, false und null folgen eigenen Regeln.
Hilft declare(strict_types=1) gegen lose Vergleiche?
Nein. strict_types steuert, ob skalare Typen bei Funktionsaufrufen und Rückgaben automatisch umgewandelt werden. Auf die Vergleichsoperatoren == und != hat es keinen Einfluss.
Wie vergleiche ich Passwörter und Tokens richtig?
Passwörter prüfen Sie mit password_verify, Tokens und Signaturen mit hash_equals. Beide vergleichen exakt und, bei hash_equals, in konstanter Zeit. Ein Vergleich mit == oder strcmp ist dafür ungeeignet.