Aktuální situace: PHP2Shell. Dvě kritické zranitelnosti PHP (CVSS 9.2) umožňují spustit škodlivý kód bez přihlášení. Záplaty od 21. 10. 2026.Co nyní udělat →

Type juggling v PHP: volná porovnání jako bezpečnostní riziko

Proč mohou volná porovnání pomocí == v PHP vést k bezpečnostním mezerám, co se změnilo s PHP 8.0 a jak psát porovnání v přihlášení, tokenech a oprávněních robustně.

Stav 5 min čteníBezpečný vývoj

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

  1. in_array() a array_search() porovnávají bez třetího parametru volně. Seznam povolených rolí nebo ID je tak měkčí, než se zdá.
  2. switch porovnává volně. Výraz match, 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.
  3. Vstupy JSON poskytují skutečné typy. Pole, které očekáváte jako řetězec, může dorazit jako true nebo jako číslo a s == se překvapivě shodovat.
  4. strcmp() pro tajné hodnoty se nehodí, protože doba běhu závisí na obsahu a chybové případy se snadno vyhodnotí špatně.
  5. Kontrola návratových hodnot: funkce jako strpos() vracejí 0 pro nález na začátku a false pro žá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.

Důležité

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 match a 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í.

Zdroje

Okamžitý kontakt

Každá hodina se počítá. Napište nám hned.

Stručně popište systém, verzi PHP a pozorování. Ozveme se, vyjasníme situaci a budeme pracovat na dálku, dokud nebude incident uzavřen a zdokumentován.

Okamžitá pomoc