Ukratko: PHP Type Juggling označava automatsku pretvorbu tipova koja kod labave usporedbe pomoću == odlučuje o rezultatu. Od PHP-a 8.0 pravila su stroža, ali numerički nizovi znakova i dalje se uspoređuju kao brojevi, a true, false i null slijede vlastita pravila. Za sigurnosno relevantne provjere stoga vrijedi: ===, in_array(..., true), match umjesto switch, hash_equals() za tokene i password_verify() za lozinke.
Usporedba koja je istinita iako ne bi smjela biti
Mnoge sigurnosne ranjivosti u PHP aplikacijama ne kriju se u složenim funkcijama, nego u jednom jedinom znaku: == tamo gdje je trebao stajati ===. Kod izgleda ispravno, testovi s normalnim unosima prolaze, a ipak provjera pod određenim uvjetima propušta nešto što je trebala zaustaviti.
Uzrok je svojstvo PHP-a koje je u svakodnevici praktično: vrijednosti se po potrebi pretvaraju u drugi tip. Niz znakova iz obrasca tako se bez zaobilaznice može usporediti s brojem iz baze podataka. Kod prijava, tokena, uloga ili statusnih polja upravo ta velikodušnost može postati opasna.
Kako PHP uspoređuje pomoću ==
PHP priručnik opširno opisuje pravila za labave usporedbe. Za sigurnost su ponajprije važni ovi slučajevi:
| Usporedba | PHP 7 | PHP 8 | Razlog |
|---|---|---|---|
0 == "a" |
istina | laž | od PHP-a 8.0 uspoređuju se kao nizovi znakova |
"1" == "01" |
istina | istina | oba numerička, usporedba kao broj |
"10" == "1e1" |
istina | istina | eksponencijalni zapis je numerički |
100 == "1e2" |
istina | istina | broj naspram numeričkog niza znakova |
null == false |
istina | istina | usporedba kao logička vrijednost |
"abc" == true |
istina | istina | neprazan niz znakova vrijedi kao istina |
Promjena u PHP-u 8.0 ublažila je cijelu klasu pogrešaka: ranije je nenumerički niz znakova pri usporedbi s brojem postajao 0. To je pogađalo i switch naredbe. Ostali redci tablice ipak pokazuju da labave usporedbe i dalje kriju iznenađenja.
Operatori <, > i operator usporedbe <=> također slijede ta pravila. Kod sortiranja to je obično bezazleno, a kod provjera poput „iznos manji od limita“ s vrijednostima iz zahtjeva tip bi trebao biti utvrđen unaprijed.
Osobito je podmukao redak s eksponencijalnim zapisom. Hash vrijednosti i tokeni često se sastoje od znamenki i slova. Ako takva vrijednost slučajno izgleda kao broj u eksponencijalnom zapisu, == dva različita hasha možda uspoređuje kao jednaka. To je pozadina mnogih izvješća o zaobilaženju prijave u starijim PHP aplikacijama.
PHP Type Juggling u tipičnim sigurnosnim provjerama
Labave usporedbe skrivaju se na više mjesta nego samo u operatoru ==:
in_array()iarray_search()bez trećeg parametra uspoređuju labavo. Popis dopuštenog s ulogama ili ID-ovima time postaje mekši nego što se misli.switchuspoređuje labavo. Izrazmatch, dostupan od PHP-a 8.0, prema priručniku pak uspoređuje tipski sigurno pomoću===i baca pogrešku ako nijedna grana ne odgovara.- JSON unosi donose prave tipove. Polje koje očekujete kao niz znakova može stići kao
trueili kao broj i pomoću==neočekivano se podudarati. strcmp()za tajne nije prikladan jer trajanje ovisi o sadržaju, a slučajevi pogreške lako se pogrešno ocijene.- Provjera povratnih vrijednosti: funkcije poput
strpos()vraćaju0za pogodak na početku ifalseza nepostojanje pogotka.if (!strpos(...))zamjenjuje jedno s drugim.
Sigurne usporedbe: usporedni prikaz
Sljedeći primjeri svaki put pokazuju nesigurnu i sigurnu varijantu:
<?php
declare(strict_types=1);
// Provjeriti ulogu
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // nesigurno: labavo
in_array($rolle, $erlaubteRollen, true); // sigurno: strogo
// Ocijeniti status
$aktion = match ($status) { // strogo, pogreška kod nepoznate vrijednosti
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Provjeriti token iz poveznice
if ($tokenAusLink == $gespeichertesToken) { /* nesigurno */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* sigurno */ }
// Provjeriti lozinku
if (md5($eingabe) == $hash) { /* nesigurno: slab postupak i labava usporedba */ }
if (password_verify($eingabe, $hash)) { /* sigurno */ }
// Tražiti podniz
if (strpos($text, 'abc') !== false) { /* ispravno: i pogodak na poziciji 0 */ }
hash_equals() prema priručniku kao prvi parametar očekuje poznatu tajnu, a kao drugi vrijednost korisnika, i uspoređuje u konstantnom vremenu. Kako tokene uopće sigurno stvarati, opisuje Sigurni slučajni brojevi i tokeni u PHP-u.
declare(strict_types=1) ne čini usporedbe strogima. Naredba se tiče samo pretvorbe tipova pri pozivima funkcija i povratnim vrijednostima. Labave usporedbe morate sami zamijeniti u kodu.
Stroge usporedbe imaju i svoju lošu stranu: propadaju kad se tipovi ne podudaraju. '42' === 42 je laž. Vrijednosti iz obrazaca i URL-ova u PHP-u stižu kao nizovi znakova, a vrijednosti iz baze podataka ovisno o driveru i postavci kao nizovi znakova ili brojevi. Tko posvuda uvede === a da ne uskladi tipove, stvara pogreške koje se u radu pokazuju kao odbijanje opravdanih korisnika.
Rješenje je jasna granica: unosi se pri primitku validiraju i dovode u ispravan tip, primjerice pomoću filter_var($wert, FILTER_VALIDATE_INT), a zatim se u kodu tretiraju samo deklaracijama tipova i strogim usporedbama. Deklaracije tipova na parametrima, povratnim vrijednostima i svojstvima tome pomažu jer se pogrešni tipovi tada rano uoče porukom o pogrešci, umjesto da tiho nestanu u usporedbi.
Sustavno provjeriti postojeći kod
U razvijenim projektima labavih usporedbi ima na stotine. Nije svaka problem. Usredotočite se na mjesta na kojima se odlučuje o pristupu:
# Labave usporedbe u retcima sa sigurnosno relevantnim pojmovima
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array bez stroge usporedbe
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Alati za statičku analizu poput PHPStana ili Psalma na zahtjev mogu trajno prijavljivati labave usporedbe. Tako novi slučajevi uopće ne dospijevaju u glavnu granu. Osnove o tipovima opisuje Sustav tipova PHP-a; kako se unosi prethodno dovode u ispravan tip, pokazuje Validirati i filtrirati unos u PHP-u.
Što to znači za stare sustave
Aplikacije koje još rade na PHP-u 7 pogođene su starim, labavijim pravilima usporedbe. Tome se pridružuje drugi problem: za PHP 8.1 i starije PHP projekt više ne daje službene sigurnosne updatee, ni protiv PHP2Shella. Stanje opisuje PHP 8.1 i starije: što sada vrijedi.
Pri migraciji na PHP 8 neki rezultati usporedbi mijenjaju se. To je obično poboljšanje, ali može slomiti kod koji se oslanjao na staro ponašanje. Zato planirajte testove koji ciljano pokrivaju usporedbe s brojevima, praznim vrijednostima i null.
Kako PHP SOS konkretno pomaže?
- Pretražujemo Vaš kod u potrazi za labavim usporedbama na mjestima koja odlučuju o prijavi, pravima ili plaćanjima.
- Zamjenjujemo ih strogim usporedbama,
matchizrazima i odgovarajućim funkcijama za provjeru. - Postavljamo statičku analizu tako da se nove labave usporedbe uoče u buildu.
- Pratimo migracije na aktualne PHP verzije testovima za promijenjena pravila usporedbe.
Provjeru Vašeg softvera opisuje Sigurnosni audit i penetracijski test.
Česta pitanja
Što je Type Juggling u PHP-u?
PHP kod nekih operacija vrijednosti različitih tipova automatski pretvara, primjerice niz znakova u broj. Kod labave usporedbe pomoću == ta pretvorba odlučuje o rezultatu. To je zgodno, ali može dovesti do usporedbi koje su istinite iako vrijednosti ne izgledaju jednako.
Je li PHP 8 riješio problem?
Djelomično. Od PHP-a 8.0 broj se s nenumeričkim nizom znakova uspoređuje kao niz znakova, dakle 0 == 'a' više nije istina. Dva numerička niza znakova ipak se i dalje uspoređuju kao brojevi, a usporedbe s true, false i null slijede vlastita pravila.
Pomaže li declare(strict_types=1) protiv labavih usporedbi?
Ne. strict_types određuje pretvaraju li se skalarni tipovi automatski pri pozivima funkcija i povratnim vrijednostima. Na operatore usporedbe == i != nema utjecaja.
Kako ispravno usporediti lozinke i tokene?
Lozinke provjeravate pomoću password_verify, tokene i potpise pomoću hash_equals. Obje uspoređuju točno i, kod hash_equals, u konstantnom vremenu. Usporedba pomoću == ili strcmp za to je neprikladna.