Stručně: Type juggling v PHP označuje automatický převod typů, který u volného porovnání pomocí == rozhoduje o výsledku. Od PHP 8.0 jsou pravidla přísnější, ale numerické řetězce se dál porovnávají jako čísla a true, false a null se řídí vlastními pravidly. Pro bezpečnostně relevantní kontroly proto platí: ===, in_array(..., true), match místo switch, hash_equals() pro tokeny a password_verify() pro hesla.
Porovnání, které platí, ačkoli by neplatit nemělo
Mnoho bezpečnostních mezer v aplikacích PHP se neskrývá ve složitých funkcích, ale v jediném znaku: v == tam, kam patřilo ===. Kód vypadá správně, testy s běžnými vstupy projdou a kontrola přesto za určitých podmínek propustí něco, co měla zastavit.
Příčinou je vlastnost PHP, která je v běžném provozu praktická: hodnoty se podle potřeby převádějí na jiný typ. Řetězec z formuláře lze tak bez oklik porovnat s číslem z databáze. U přihlášení, tokenů, rolí nebo stavových polí se právě tato velkorysost může stát nebezpečnou.
Jak PHP porovnává pomocí ==
Příručka PHP podrobně popisuje pravidla pro volná porovnání. Pro bezpečnost jsou důležité hlavně tyto případy:
| Porovnání | PHP 7 | PHP 8 | Důvod |
|---|---|---|---|
0 == "a" |
pravda | nepravda | od PHP 8.0 se porovnává jako řetězce |
"1" == "01" |
pravda | pravda | oba numerické, porovnání jako číslo |
"10" == "1e1" |
pravda | pravda | exponenciální zápis je numerický |
100 == "1e2" |
pravda | pravda | číslo proti numerickému řetězci |
null == false |
pravda | pravda | porovnání jako pravdivostní hodnota |
"abc" == true |
pravda | pravda | neprázdný řetězec platí jako pravda |
Změna v PHP 8.0 zmírnila celou třídu chyb: dříve se nenumerický řetězec při porovnání s číslem změnil na 0. Týkalo se to i příkazů switch. Ostatní řádky tabulky ale ukazují, že volná porovnání dál skrývají překvapení.
Podle těchto pravidel se řídí i operátory <, > a operátor porovnání <=>. U řazení je to většinou neškodné, u kontrol typu „částka menší než limit“ s hodnotami z požadavku by měl být typ znám předem.
Obzvlášť záludný je řádek s exponenciálním zápisem. Hodnoty hashů a tokeny se často skládají z číslic a písmen. Pokud takto hodnota náhodou vypadá jako číslo v exponenciálním zápisu, == může dva různé hashe považovat za shodné. To je pozadí mnoha zpráv o obcházení přihlášení ve starších aplikacích PHP.
Type juggling v PHP v typických bezpečnostních kontrolách
Volná porovnání se skrývají na více místech než jen v operátoru ==:
in_array()aarray_search()porovnávají bez třetího parametru volně. Seznam povolených rolí nebo ID je tak měkčí, než se zdá.switchporovnává volně. Výrazmatch, dostupný od PHP 8.0, naproti tomu podle příručky porovnává typově bezpečně pomocí===a vyvolá chybu, pokud nesedí žádná větev.- Vstupy JSON poskytují skutečné typy. Pole, které očekáváte jako řetězec, může dorazit jako
truenebo jako číslo a s==se překvapivě shodovat. strcmp()pro tajné hodnoty se nehodí, protože doba běhu závisí na obsahu a chybové případy se snadno vyhodnotí špatně.- Kontrola návratových hodnot: funkce jako
strpos()vracejí0pro nález na začátku afalsepro žádný nález.if (!strpos(...))obojí zaměňuje.
Bezpečná porovnání: srovnání
Následující příklady ukazují vždy nebezpečnou a bezpečnou variantu:
<?php
declare(strict_types=1);
// Zkontrolovat roli
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // nebezpečné: volně
in_array($rolle, $erlaubteRollen, true); // bezpečné: striktně
// Vyhodnotit stav
$aktion = match ($status) { // striktně, chyba při neznámé hodnotě
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Zkontrolovat token z odkazu
if ($tokenAusLink == $gespeichertesToken) { /* nebezpečné */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* bezpečné */ }
// Zkontrolovat heslo
if (md5($eingabe) == $hash) { /* nebezpečné: slabá metoda a volné porovnání */ }
if (password_verify($eingabe, $hash)) { /* bezpečné */ }
// Hledat podřetězec
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() očekává podle příručky jako první parametr známé tajemství a jako druhý hodnotu uživatele a porovnává v konstantním čase. Jak tokeny vůbec bezpečně vytvářet, popisuje článek Bezpečná náhodná čísla a tokeny v PHP.
declare(strict_types=1) nečiní porovnání striktními. Příkaz se týká jen převodu typů u volání funkcí a návratových hodnot. Volná porovnání musíte v kódu nahradit sami.
Striktní porovnání mají druhou stranu: selhávají, když typy nesedí. '42' === 42 je nepravda. Hodnoty z formulářů a URL přicházejí v PHP jako řetězce, hodnoty z databáze podle ovladače a nastavení jako řetězce nebo čísla. Kdo všude zavede ===, aniž by typy sjednotil, vytvoří chyby, které se v provozu projeví jako odmítnutí oprávněných uživatelů.
Řešením je jasná hranice: vstupy se při příjmu validují a převedou na správný typ, například pomocí filter_var($wert, FILTER_VALIDATE_INT), a poté se v kódu zpracovávají už jen s deklaracemi typů a striktními porovnáními. Deklarace typů u parametrů, návratových hodnot a vlastností k tomu pomáhají, protože chybné typy pak vyjdou brzy najevo s chybovým hlášením, místo aby tiše zmizely v porovnání.
Systematická kontrola stávajícího kódu
V rozrostlých projektech se volná porovnání vyskytují po stovkách. Ne každé je problém. Soustřeďte se na místa, kde se rozhoduje o přístupu:
# Volná porovnání na řádcích s bezpečnostně relevantními pojmy
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array bez striktního porovnání
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Nástroje statické analýzy jako PHPStan nebo Psalm mohou volná porovnání na požádání trvale hlásit. Nové případy se tak do hlavní větve vůbec nedostanou. Základy o typech popisuje Typový systém PHP; jak se vstupy předem převedou na správný typ, ukazuje Validace a filtrování vstupů v PHP.
Co to znamená pro starší systémy
Aplikace, které stále běží na PHP 7, jsou zasaženy starými, uvolněnějšími pravidly porovnávání. K tomu přistupuje druhý problém: pro PHP 8.1 a starší už projekt PHP nevydává oficiální bezpečnostní aktualizace, ani proti PHP2Shell. Situaci popisuje stránka PHP 8.1 a starší: co platí nyní.
Při migraci na PHP 8 se některé výsledky porovnání mění. Je to většinou zlepšení, ale může to rozbít kód, který se spoléhal na staré chování. Počítejte proto s testy, které cíleně pokrývají porovnání s čísly, prázdnými hodnotami a null.
Jak konkrétně pomáhá PHP SOS?
- Prohledáme váš kód na volná porovnání v místech, kde se rozhoduje o přihlášení, právech nebo platbách.
- Nahradíme je striktními porovnáními, výrazy
matcha vhodnými kontrolními funkcemi. - Nastavíme statickou analýzu tak, aby nová volná porovnání vyšla najevo při sestavení.
- Migrace na aktuální verze PHP doprovodíme testy pro změněná pravidla porovnávání.
Kontrolu vašeho softwaru popisuje bezpečnostní audit a penetrační test.
Časté dotazy
Co je type juggling v PHP?
PHP u některých operací automaticky převádí hodnoty různých typů, například řetězec na číslo. U volného porovnání pomocí == rozhoduje o výsledku právě tento převod. Je to pohodlné, ale může to vést k porovnáním, která jsou pravdivá, i když hodnoty nevypadají stejně.
Vyřešilo PHP 8 tento problém?
Částečně. Od PHP 8.0 se číslo s nenumerickým řetězcem porovnává jako řetězec, takže 0 == 'a' už neplatí. Dva numerické řetězce se ale dál porovnávají jako čísla a porovnání s true, false a null se řídí vlastními pravidly.
Pomáhá declare(strict_types=1) proti volným porovnáním?
Ne. strict_types řídí, zda se skalární typy při voláních funkcí a návratech převádějí automaticky. Na operátory porovnání == a != nemá vliv.
Jak správně porovnávat hesla a tokeny?
Hesla ověřujte pomocí password_verify, tokeny a podpisy pomocí hash_equals. Obě porovnávají přesně a hash_equals v konstantním čase. Porovnání pomocí == nebo strcmp se k tomu nehodí.