Stručne: PHP Type Juggling označuje automatický prevod typov, ktorý pri voľnom porovnaní pomocou == rozhoduje o výsledku. Od PHP 8.0 sú pravidlá prísnejšie, ale číselné reťazce sa naďalej porovnávajú ako čísla a true, false a null sa riadia vlastnými pravidlami. Pre porovnania dôležité z hľadiska bezpečnosti preto platí: ===, in_array(..., true), match namiesto switch, hash_equals() pre tokeny a password_verify() pre heslá.
Porovnanie, ktoré je pravdivé, hoci by nemalo byť
Mnohé bezpečnostné zraniteľnosti v aplikáciách PHP nesedia v komplikovaných funkciách, ale v jedinom znaku: v ==, kde patrilo ===. Kód vyzerá správne, testy s bežnými vstupmi prejdú a napriek tomu kontrola za určitých podmienok prepustí niečo, čo mala zastaviť.
Príčinou je vlastnosť PHP, ktorá je v každodennom živote praktická: hodnoty sa podľa potreby prevádzajú na iný typ. Reťazec z formulára sa tak dá bez okľuky porovnať s číslom z databázy. Pri prihláseniach, tokenoch, rolách alebo stavových poliach môže byť práve táto veľkorysosť nebezpečná.
Ako PHP porovnáva pomocou ==
Príručka PHP podrobne opisuje pravidlá voľných porovnaní. Pre bezpečnosť sú dôležité najmä tieto prípady:
| Porovnanie | PHP 7 | PHP 8 | Dôvod |
|---|---|---|---|
0 == "a" |
pravda | nepravda | od PHP 8.0 sa porovnáva ako reťazce |
"1" == "01" |
pravda | pravda | obe číselné, porovnanie ako číslo |
"10" == "1e1" |
pravda | pravda | exponenciálny zápis je číselný |
100 == "1e2" |
pravda | pravda | číslo voči číselnému reťazcu |
null == false |
pravda | pravda | porovnanie ako pravdivostná hodnota |
"abc" == true |
pravda | pravda | neprázdny reťazec platí ako pravda |
Zmena v PHP 8.0 zmiernila celú triedu chýb: predtým sa nečíselný reťazec pri porovnaní s číslom stal 0. Týkalo sa to aj príkazov switch. Ostatné riadky tabuľky však ukazujú, že voľné porovnania naďalej skrývajú prekvapenia.
Aj operátory <, > a porovnávací operátor <=> sa riadia týmito pravidlami. Pri triedení je to zvyčajne neškodné, pri kontrolách ako „suma menšia než limit“ s hodnotami z požiadavky by mal byť typ vopred určený.
Obzvlášť zákerný je riadok s exponenciálnym zápisom. Hodnoty hashov a tokeny často pozostávajú z číslic a písmen. Ak taká hodnota náhodou vyzerá ako číslo v exponenciálnom zápise, == môže dva rôzne hashe porovnať ako rovnaké. To je pozadie mnohých správ o obchádzaní prihlásenia v starších aplikáciách PHP.
PHP Type Juggling v typických bezpečnostných kontrolách
Voľné porovnania sa skrývajú na viacerých miestach než len v operátore ==:
in_array()aarray_search()porovnávajú bez tretieho parametra voľne. Zoznam povolených rolí alebo ID sa tým stáva mäkším, než sa čaká.switchporovnáva voľne. Výrazmatchdostupný od PHP 8.0 naopak podľa príručky porovnáva typovo bezpečne pomocou===a vyhodí chybu, ak nesedí žiadna vetva.- Vstupy JSON dodávajú skutočné typy. Pole, ktoré očakávate ako reťazec, môže prísť ako
truealebo ako číslo a pri==sa prekvapivo zhodovať. strcmp()pre tajomstvá je nevhodná, pretože čas behu závisí od obsahu a chybové prípady sa ľahko vyhodnotia nesprávne.- Kontrola návratových hodnôt: funkcie ako
strpos()vracajú0pre zhodu na začiatku afalsepre žiadnu zhodu.if (!strpos(...))oboje zamieňa.
Bezpečné porovnania: porovnanie vedľa seba
Nasledujúce príklady ukazujú vždy nebezpečný a bezpečný variant:
<?php
declare(strict_types=1);
// Skontrolovať rolu
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // nebezpečné: voľne
in_array($rolle, $erlaubteRollen, true); // bezpečné: striktne
// Vyhodnotiť stav
$aktion = match ($status) { // striktne, chyba pri neznámej hodnote
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Skontrolovať token z odkazu
if ($tokenAusLink == $gespeichertesToken) { /* nebezpečné */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* bezpečné */ }
// Skontrolovať heslo
if (md5($eingabe) == $hash) { /* nebezpečné: slabý algoritmus a voľné porovnanie */ }
if (password_verify($eingabe, $hash)) { /* bezpečné */ }
// Hľadať podreťazec
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() očakáva podľa príručky ako prvý parameter známe tajomstvo a ako druhý hodnotu používateľa a porovnáva v konštantnom čase. Ako tokeny vôbec bezpečne vytvárať, opisuje Bezpečné náhodné čísla a tokeny v PHP.
declare(strict_types=1) porovnania nerobí striktnými. Príkaz sa týka len prevodu typov pri volaniach funkcií a návratových hodnotách. Voľné porovnania musíte v kóde nahradiť sami.
Striktné porovnania majú aj druhú stranu: zlyhajú, keď typy nesedia. '42' === 42 je nepravda. Hodnoty z formulárov a URL prichádzajú v PHP ako reťazce, hodnoty z databázy podľa ovládača a nastavenia ako reťazce alebo čísla. Kto všade zavedie === bez zosúladenia typov, vytvorí chyby, ktoré sa v prevádzke prejavia ako odmietnutie oprávnených používateľov.
Riešením je jasná hranica: vstupy sa pri prijatí validujú a uvedú na správny typ, napríklad pomocou filter_var($wert, FILTER_VALIDATE_INT), a potom sa v kóde spracúvajú už len s deklaráciami typov a striktnými porovnaniami. Deklarácie typov pri parametroch, návratových hodnotách a vlastnostiach tomu pomáhajú, pretože nesprávne typy potom vyjdú najavo včas s chybovým hlásením, namiesto toho, aby potichu zmizli v porovnaní.
Systematická kontrola existujúceho kódu
V prerastených projektoch nájdete voľné porovnania v stovkách. Nie každé je problém. Sústreďte sa na miesta, kde sa rozhoduje o prístupe:
# Voľné porovnania v riadkoch s bezpečnostne relevantnými pojmami
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array bez striktného porovnania
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Nástroje statickej analýzy ako PHPStan alebo Psalm môžu na požiadanie hlásiť voľné porovnania trvalo. Nové prípady sa tak vôbec nedostanú do hlavnej vetvy. Základy o typoch opisuje Typový systém PHP; ako sa vstupy vopred uvedú na správny typ, ukazuje Validácia a filtrovanie vstupov v PHP.
Čo to znamená pre staré systémy
Aplikácie, ktoré ešte bežia na PHP 7, sú dotknuté starými, voľnejšími pravidlami porovnávania. K tomu pristupuje druhý problém: pre PHP 8.1 a staršie už projekt PHP neposkytuje oficiálne bezpečnostné aktualizácie, ani proti PHP2Shell. Situáciu opisuje PHP 8.1 a staršie: čo platí teraz.
Pri migrácii na PHP 8 sa niektoré výsledky porovnaní menia. Je to zvyčajne zlepšenie, ale môže to pokaziť kód, ktorý sa spoliehal na staré správanie. Naplánujte preto testy, ktoré cielene pokrývajú porovnania s číslami, prázdnymi hodnotami a null.
Ako konkrétne pomáha PHP SOS?
- Prehľadáme váš kód na voľné porovnania na miestach, ktoré rozhodujú o prihlásení, právach alebo platbách.
- Nahradíme ich striktnými porovnaniami, výrazmi
matcha vhodnými kontrolnými funkciami. - Nastavíme statickú analýzu tak, aby sa nové voľné porovnania odhalili v buildu.
- Migrácie na aktuálne verzie PHP sprevádzame testami pre zmenené pravidlá porovnávania.
Kontrolu vášho softvéru opisuje Bezpečnostný audit a penetračný test.
Časté otázky
Čo je Type Juggling v PHP?
PHP pri niektorých operáciách automaticky prevádza hodnoty rôznych typov, napríklad reťazec na číslo. Pri voľnom porovnaní pomocou == o výsledku rozhoduje práve tento prevod. Je to pohodlné, ale môže viesť k porovnaniam, ktoré sú pravdivé, hoci hodnoty nevyzerajú rovnako.
Vyriešilo PHP 8 tento problém?
Čiastočne. Od PHP 8.0 sa číslo s nečíselným reťazcom porovnáva ako reťazec, takže 0 == 'a' už nie je pravda. Dva číselné reťazce sa však naďalej porovnávajú ako čísla a porovnania s true, false a null sa riadia vlastnými pravidlami.
Pomáha declare(strict_types=1) proti voľným porovnaniam?
Nie. strict_types riadi, či sa skalárne typy pri volaniach funkcií a návratoch automaticky prevádzajú. Na porovnávacie operátory == a != nemá vplyv.
Ako správne porovnávať heslá a tokeny?
Heslá overujte pomocou password_verify, tokeny a podpisy pomocou hash_equals. Obe porovnávajú presne a hash_equals v konštantnom čase. Porovnanie pomocou == alebo strcmp je na to nevhodné.