# Type juggling w PHP: luźne porównania jako ryzyko bezpieczeństwa

> Type juggling w PHP: jak porównuje ==, co zmieniło PHP 8.0 i dlaczego tokeny, skróty i role trzeba porównywać ściśle. Z przykładami i wyszukiwaniem w kodzie.

Źródło: https://phpsos.de/pl/blog/type-juggling-php/  
Stan: 25.10.2026  
Dostawca: PHP SOS, Pomoc awaryjna PHP i bezpieczeństwo serwerów. Natychmiastowa pomoc przez WhatsApp: https://wa.me/491736494914?text=Dzie%C5%84%20dobry%2C%20PHP%20SOS%2C%20potrzebujemy%20pomocy%20z%20naszym%20serwerem%20PHP.%20Firma%3A%20%E2%80%A6%20Strona%20internetowa%3A%20%E2%80%A6

**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
<?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](https://phpsos.de/pl/blog/sichere-tokens-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:

```bash
# 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](https://phpsos.de/pl/php/typsystem/); jak wcześniej przekształcić dane wejściowe na właściwy typ, pokazuje artykuł [Walidacja i filtrowanie danych wejściowych w PHP](https://phpsos.de/pl/blog/eingaben-validieren-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](https://phpsos.de/pl/php2shell/php-8-1-und-aelter/).

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](https://phpsos.de/pl/leistungen/sicherheitsaudit-penetrationstest/).


## 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

- [Podręcznik PHP: operatory porównania](https://www.php.net/manual/de/language.operators.comparison.php)
- [Podręcznik PHP: match](https://www.php.net/manual/de/control-structures.match.php)
- [Seria ściągawek OWASP (Cheat Sheet Series)](https://cheatsheetseries.owasp.org/)
