Aktualna sytuacja: PHP2Shell. Dwie krytyczne luki w PHP (CVSS 9,2) umożliwiają wykonanie złośliwego kodu bez logowania. Poprawki od 21.10.2026.Co należy teraz zrobić →

Type juggling w PHP: luźne porównania jako ryzyko bezpieczeństwa

Dlaczego luźne porównania za pomocą == w PHP mogą prowadzić do luk w zabezpieczeniach, co zmieniło się w PHP 8.0 i jak pisać odporne porównania w logowaniu, tokenach i uprawnieniach.

Stan 5 min czytaniaBezpieczne programowanie

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 ==:

  1. in_array() i array_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.
  2. switch porównuje luźno. Dostępne od PHP 8.0 wyrażenie match porównuje według podręcznika natomiast bezpiecznie pod względem typów za pomocą === i zgłasza błąd, gdy żadna gałąź nie pasuje.
  3. Dane wejściowe JSON dostarczają prawdziwych typów. Pole, które oczekujesz jako ciąg znaków, może dotrzeć jako true lub liczba i zaskakująco zgodzić się z ==.
  4. 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.
  5. Sprawdzanie wartości zwracanych: funkcje takie jak strpos() zwracają 0 dla trafienia na początku i false dla 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.

Ważne

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 match i 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.

Źródła

Kontakt natychmiast

Liczy się każda godzina. Proszę napisać do nas teraz.

Proszę krótko opisać system, wersję PHP i obserwacje. Skontaktujemy się z Państwem, ustalimy sytuację i będziemy pracować zdalnie, aż incydent zostanie zamknięty i udokumentowany.

Natychmiastowa pomoc