Krótko: Type juggling w PHP oznacza automatyczną zamianę typów, która przy luźnym porównaniu za pomocą == decyduje o wyniku. Od PHP 8.0 reguły są ostrzejsze, ale numeryczne ciągi znaków są nadal porównywane jako liczby, a true, false i null podlegają własnym regułom. Dla kontroli istotnych dla bezpieczeństwa obowiązuje więc: ===, in_array(..., true), match zamiast switch, hash_equals() dla tokenów i password_verify() dla haseł.
Porównanie, które jest prawdziwe, choć nie powinno być
Wiele luk bezpieczeństwa w aplikacjach PHP tkwi nie w skomplikowanych funkcjach, lecz w jednym znaku: w == tam, gdzie powinno być ===. Kod wygląda poprawnie, testy ze zwykłymi danymi przechodzą, a mimo to kontrola w pewnych warunkach przepuszcza coś, co powinna zatrzymać.
Przyczyną jest cecha PHP, która na co dzień jest praktyczna: wartości są w razie potrzeby zamieniane na inny typ. Ciąg znaków z formularza można dzięki temu bez dodatkowych kroków porównać z liczbą z bazy danych. Przy logowaniu, tokenach, rolach lub polach statusu właśnie ta hojność może być niebezpieczna.
Jak PHP porównuje za pomocą ==
Podręcznik PHP szczegółowo opisuje reguły luźnych porównań. Dla bezpieczeństwa ważne są przede wszystkim te przypadki:
| Porównanie | PHP 7 | PHP 8 | Powód |
|---|---|---|---|
0 == "a" |
prawda | fałsz | od PHP 8.0 porównywane jako ciągi znaków |
"1" == "01" |
prawda | prawda | oba numeryczne, porównanie jako liczba |
"10" == "1e1" |
prawda | prawda | zapis wykładniczy jest numeryczny |
100 == "1e2" |
prawda | prawda | liczba wobec numerycznego ciągu znaków |
null == false |
prawda | prawda | porównanie jako wartość logiczna |
"abc" == true |
prawda | prawda | niepusty ciąg znaków uchodzi za prawdę |
Zmiana w PHP 8.0 złagodziła całą klasę błędów: wcześniej nienumeryczny ciąg znaków przy porównaniu z liczbą stawał się 0. Dotyczyło to także instrukcji switch. Pozostałe wiersze tabeli pokazują jednak, że luźne porównania nadal kryją niespodzianki.
Także operatory <, > i operator porównania <=> podlegają tym regułom. Przy sortowaniu jest to zwykle nieszkodliwe, ale przy kontrolach takich jak „kwota mniejsza niż limit” z wartościami z żądania typ powinien być ustalony wcześniej.
Szczególnie podstępny jest wiersz z zapisem wykładniczym. Skróty i tokeny często składają się z cyfr i liter. Jeśli taka wartość przypadkiem wygląda jak liczba w zapisie wykładniczym, == może uznać dwa różne skróty za równe. To tło wielu doniesień o obejściach logowania w starszych aplikacjach PHP.
Type juggling w PHP w typowych kontrolach bezpieczeństwa
Luźne porównania ukrywają się w większej liczbie miejsc niż tylko w operatorze ==:
in_array()iarray_search()porównują bez trzeciego parametru luźno. Lista dozwolonych ról lub identyfikatorów staje się przez to miększa, niż się sądzi.switchporównuje luźno. Dostępne od PHP 8.0 wyrażeniematchporównuje według podręcznika natomiast bezpiecznie pod względem typów za pomocą===i zgłasza błąd, gdy żadna gałąź nie pasuje.- Dane wejściowe JSON dostarczają prawdziwych typów. Pole, które oczekujesz jako ciąg znaków, może dotrzeć jako
truelub liczba i zaskakująco zgodzić się z==. strcmp()dla sekretów się nie nadaje, ponieważ czas działania zależy od treści, a przypadki błędów łatwo ocenić niewłaściwie.- Sprawdzanie wartości zwracanych: funkcje takie jak
strpos()zwracają0dla trafienia na początku ifalsedla braku trafienia.if (!strpos(...))myli jedno z drugim.
Bezpieczne porównania: zestawienie
Poniższe przykłady pokazują każdorazowo wariant niebezpieczny i bezpieczny:
<?php
declare(strict_types=1);
// Sprawdzić rolę
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // niebezpieczne: luźno
in_array($rolle, $erlaubteRollen, true); // bezpieczne: ściśle
// Ocenić status
$aktion = match ($status) { // ściśle, błąd przy nieznanej wartości
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Sprawdzić token z linku
if ($tokenAusLink == $gespeichertesToken) { /* niebezpieczne */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* bezpieczne */ }
// Sprawdzić hasło
if (md5($eingabe) == $hash) { /* niebezpieczne: słaba metoda i luźne porównanie */ }
if (password_verify($eingabe, $hash)) { /* bezpieczne */ }
// Wyszukać podciąg
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() oczekuje według podręcznika znanego sekretu jako pierwszego, a wartości użytkownika jako drugiego parametru i porównuje w stałym czasie. Jak w ogóle bezpiecznie generować tokeny, opisuje artykuł Bezpieczne liczby losowe i tokeny w PHP.
declare(strict_types=1) nie sprawia, że porównania są ścisłe. Instrukcja dotyczy tylko zamiany typów przy wywołaniach funkcji i wartościach zwracanych. Luźne porównania musisz zastąpić w kodzie samodzielnie.
Ścisłe porównania mają drugą stronę: zawodzą, gdy typy do siebie nie pasują. '42' === 42 jest fałszem. Wartości z formularzy i adresów URL docierają w PHP jako ciągi znaków, wartości z bazy danych zależnie od sterownika i ustawień jako ciągi znaków lub liczby. Kto wprowadza wszędzie ===, nie ujednolicając typów, wywołuje błędy, które w działaniu objawiają się odrzucaniem uprawnionych użytkowników.
Rozwiązaniem jest wyraźna granica: dane wejściowe są walidowane przy odbiorze i przekształcane na właściwy typ, na przykład za pomocą filter_var($wert, FILTER_VALIDATE_INT), a potem w kodzie obsługiwane już tylko z deklaracjami typów i ścisłymi porównaniami. Deklaracje typów przy parametrach, wartościach zwracanych i właściwościach pomagają w tym, ponieważ błędne typy wychodzą wtedy wcześnie z komunikatem o błędzie, zamiast po cichu zniknąć w porównaniu.
Systematyczne sprawdzanie istniejącego kodu
W rozrośniętych projektach luźnych porównań są setki. Nie każde jest problemem. Skup się na miejscach, w których zapada decyzja o dostępie:
# Luźne porównania w wierszach z pojęciami istotnymi dla bezpieczeństwa
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array bez ścisłego porównania
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Narzędzia do analizy statycznej, takie jak PHPStan czy Psalm, mogą na życzenie stale zgłaszać luźne porównania. Dzięki temu nowe przypadki w ogóle nie trafiają do głównej gałęzi. Podstawy typów opisuje artykuł System typów w PHP; jak wcześniej przekształcić dane wejściowe na właściwy typ, pokazuje artykuł Walidacja i filtrowanie danych wejściowych w PHP.
Co to oznacza dla starych systemów
Aplikacje, które nadal działają na PHP 7, są dotknięte dawnymi, luźniejszymi regułami porównań. Dochodzi drugi problem: dla PHP 8.1 i starszych projekt PHP nie wydaje już oficjalnych aktualizacji bezpieczeństwa, także przeciw PHP2Shell. Sytuację opisuje artykuł PHP 8.1 i starsze: co teraz obowiązuje.
Przy migracji na PHP 8 zmieniają się niektóre wyniki porównań. Zwykle jest to poprawa, ale może zepsuć kod, który polegał na starym zachowaniu. Zaplanuj więc testy, które celowo obejmują porównania z liczbami, pustymi wartościami i null.
Jak konkretnie pomaga PHP SOS?
- Przeszukujemy Państwa kod pod kątem luźnych porównań w miejscach, które decydują o logowaniu, uprawnieniach lub płatnościach.
- Zastępujemy je ścisłymi porównaniami, wyrażeniami
matchi odpowiednimi funkcjami kontrolnymi. - Konfigurujemy analizę statyczną tak, aby nowe luźne porównania były widoczne w buildzie.
- Wspieramy migracje na aktualne wersje PHP testami dla zmienionych reguł porównań.
Kontrolę Państwa oprogramowania opisuje usługa audyt bezpieczeństwa i test penetracyjny.
Częste pytania
Czym jest type juggling w PHP?
PHP w niektórych operacjach automatycznie zamienia wartości różnych typów, na przykład ciąg znaków na liczbę. Przy luźnym porównaniu za pomocą == o wyniku decyduje ta zamiana. Jest to wygodne, ale może prowadzić do porównań, które są prawdziwe, choć wartości nie wyglądają na równe.
Czy PHP 8 rozwiązało ten problem?
Częściowo. Od PHP 8.0 liczba jest porównywana z nienumerycznym ciągiem znaków jako ciąg znaków, więc 0 == 'a' nie jest już prawdziwe. Dwa numeryczne ciągi znaków są jednak nadal porównywane jako liczby, a porównania z true, false i null podlegają własnym regułom.
Czy declare(strict_types=1) pomaga przeciw luźnym porównaniom?
Nie. strict_types steruje tym, czy typy skalarne są automatycznie zamieniane przy wywołaniach funkcji i zwracanych wartościach. Na operatory porównania == i != nie ma wpływu.
Jak prawidłowo porównywać hasła i tokeny?
Hasła sprawdzasz za pomocą password_verify, tokeny i podpisy za pomocą hash_equals. Oba porównują dokładnie, a hash_equals w stałym czasie. Porównanie za pomocą == lub strcmp się do tego nie nadaje.