Na kratko: Type juggling v PHP je samodejna pretvorba tipov, ki pri ohlapni primerjavi z == odloča o rezultatu. Od PHP 8.0 so pravila strožja, a numerični nizi se še vedno primerjajo kot števila, true, false in null pa sledijo lastnim pravilom. Za varnostno pomembna preverjanja zato velja: ===, in_array(..., true), match namesto switch, hash_equals() za žetone in password_verify() za gesla.
Primerjava, ki je resnična, čeprav ne bi smela biti
Mnoge varnostne ranljivosti v aplikacijah PHP ne tičijo v zapletenih funkcijah, ampak v enem samem znaku: v ==, kjer bi moral stati ===. Koda izgleda pravilna, preizkusi z običajnimi vnosi uspejo, a preverjanje pod določenimi pogoji kljub temu spusti skozi nekaj, kar bi moralo ustaviti.
Vzrok je lastnost PHP, ki je v vsakdanu praktična: vrednosti se po potrebi pretvorijo v drug tip. Niz iz obrazca je tako mogoče brez ovinkov primerjati s številom iz baze podatkov. Pri prijavah, žetonih, vlogah ali poljih stanja ravno ta radodarnost lahko postane nevarna.
Kako PHP primerja z ==
Priročnik PHP pravila za ohlapne primerjave podrobno opisuje. Za varnost so predvsem pomembni ti primeri:
| Primerjava | PHP 7 | PHP 8 | Razlog |
|---|---|---|---|
0 == "a" |
resnično | neresnično | od PHP 8.0 se primerjata kot niza |
"1" == "01" |
resnično | resnično | oba numerična, primerjava kot število |
"10" == "1e1" |
resnično | resnično | eksponentni zapis je numeričen |
100 == "1e2" |
resnično | resnično | število proti numeričnemu nizu |
null == false |
resnično | resnično | primerjava kot logična vrednost |
"abc" == true |
resnično | resnično | neprazen niz velja za resničnega |
Sprememba v PHP 8.0 je omilila cel razred napak: prej se je nenumerični niz pri primerjavi s številom pretvoril v 0. To je zadevalo tudi stavke switch. Preostale vrstice tabele pa kažejo, da ohlapne primerjave še vedno skrivajo presenečenja.
Tudi operatorja <, > in primerjalni operator <=> sledijo tem pravilom. Pri razvrščanju je to večinoma neškodljivo, pri preverjanjih, kot je „znesek manjši od limita“, z vrednostmi iz zahteve pa naj bo tip določen že prej.
Posebej zahrbtna je vrstica z eksponentnim zapisom. Zgoščene vrednosti in žetoni pogosto sestojijo iz številk in črk. Če taka vrednost po naključju izgleda kot število v eksponentnem zapisu, == dve različni zgoščeni vrednosti morda primerja kot enaki. To je ozadje številnih poročil o obhodih prijave v starejših aplikacijah PHP.
Type juggling v PHP pri tipičnih varnostnih preverjanjih
Ohlapne primerjave se skrivajo na več mestih kot samo v operatorju ==:
in_array()inarray_search()brez tretjega parametra primerjata ohlapno. Dovoljeni seznam z vlogami ali ID-ji je s tem mehkejši, kot bi mislili.switchprimerja ohlapno. Izrazmatch, ki je na voljo od PHP 8.0, pa po priročniku primerja tipsko varno z===in sproži napako, če nobena veja ne ustreza.- Vnosi JSON dajo prave tipe. Polje, ki ga pričakujete kot niz, lahko prispe kot
trueali kot število in se z==presenetljivo ujema. strcmp()za skrivnosti je neprimeren, ker je čas izvajanja odvisen od vsebine in se primeri napak zlahka napačno ovrednotijo.- Preverjanje vrnjenih vrednosti: funkcije, kot je
strpos(), vrnejo0za zadetek na začetku infalseza brez zadetka.if (!strpos(...))oboje zamenja.
Varne primerjave: primerjava ob strani
Naslednji primeri vsakič kažejo nevarno in varno različico:
<?php
declare(strict_types=1);
// Preverjanje vloge
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // nevarno: ohlapno
in_array($rolle, $erlaubteRollen, true); // varno: strogo
// Ovrednotenje stanja
$aktion = match ($status) { // strogo, napaka pri neznani vrednosti
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Preverjanje žetona iz povezave
if ($tokenAusLink == $gespeichertesToken) { /* nevarno */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* varno */ }
// Preverjanje gesla
if (md5($eingabe) == $hash) { /* nevarno: šibek algoritem in ohlapna primerjava */ }
if (password_verify($eingabe, $hash)) { /* varno */ }
// Iskanje podniza
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() po priročniku kot prvi parameter pričakuje znano skrivnost, kot drugega pa vrednost uporabnika, in primerja v konstantnem času. Kako žetone sploh varno ustvarite, opisuje Varna naključna števila in žetoni v PHP.
declare(strict_types=1) primerjav ne naredi strogih. Ukaz zadeva samo pretvorbo tipov pri klicih funkcij in vrnjenih vrednostih. Ohlapne primerjave morate v kodi nadomestiti sami.
Stroge primerjave imajo tudi slabo stran: spodletijo, če se tipi ne ujemajo. '42' === 42 je neresnično. Vrednosti iz obrazcev in URL-jev v PHP prispejo kot nizi, vrednosti iz baze podatkov glede na gonilnik in nastavitev kot nizi ali števila. Kdor povsod uvede ===, ne da bi tipe uskladil, ustvari napake, ki se v delovanju kažejo kot zavrnitev upravičenih uporabnikov.
Rešitev je jasna meja: vnosi se ob prejemu preverijo in pretvorijo v pravi tip, na primer s filter_var($wert, FILTER_VALIDATE_INT), nato pa se v kodi obravnavajo samo še z deklaracijami tipov in strogimi primerjavami. Deklaracije tipov pri parametrih, vrnjenih vrednostih in lastnostih pri tem pomagajo, ker se napačni tipi tako zgodaj pokažejo z napako, namesto da bi tiho izginili v primerjavi.
Sistematičen pregled obstoječe kode
V zraslih projektih se ohlapne primerjave najdejo v stotinah. Vse niso težava. Osredotočite se na mesta, kjer pade odločitev o dostopu:
# Ohlapne primerjave v vrsticah z varnostno pomembnimi izrazi
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array brez stroge primerjave
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Orodja za statično analizo, kot sta PHPStan ali Psalm, lahko ohlapne primerjave na željo trajno javljajo. Tako novi primeri sploh ne pridejo v glavno vejo. Osnove o tipih opisuje Sistem tipov v PHP; kako vnose prej pretvorite v pravi tip, kaže Preverjanje in filtriranje vnosov v PHP.
Kaj to pomeni za stare sisteme
Aplikacije, ki še tečejo na PHP 7, so prizadete zaradi starih, ohlapnejših pravil primerjav. K temu pride druga težava: za PHP 8.1 in starejše projekt PHP ne izdaja več uradnih varnostnih posodobitev, tudi proti PHP2Shell ne. Stanje opisuje PHP 8.1 in starejši: kaj velja zdaj.
Pri migraciji na PHP 8 se nekateri rezultati primerjav spremenijo. To je večinoma izboljšanje, lahko pa pokvari kodo, ki se je zanašala na staro vedenje. Zato načrtujte preizkuse, ki ciljno pokrivajo primerjave s števili, praznimi vrednostmi in null.
Kako konkretno pomaga PHP SOS?
- V vaši kodi poiščemo ohlapne primerjave na mestih, ki odločajo o prijavi, pravicah ali plačilih.
- Nadomestimo jih s strogimi primerjavami, izrazi
matchin ustreznimi funkcijami za preverjanje. - Statično analizo nastavimo tako, da nove ohlapne primerjave opazimo že v gradnji.
- Migracije na aktualne različice PHP spremljamo s preizkusi za spremenjena pravila primerjav.
Pregled vaše programske opreme opisuje Varnostni pregled in penetracijski test.
Pogosta vprašanja
Kaj je type juggling v PHP?
PHP pri nekaterih operacijah vrednosti različnih tipov samodejno pretvori, na primer niz v število. Pri ohlapni primerjavi z == o rezultatu odloča ta pretvorba. To je udobno, a lahko vodi do primerjav, ki so resnične, čeprav vrednosti ne izgledajo enako.
Ali je PHP 8 težavo rešil?
Delno. Od PHP 8.0 se število z nenumeričnim nizom primerja kot niz, torej 0 == 'a' ni več resnično. Dva numerična niza pa se še vedno primerjata kot števili, primerjave s true, false in null pa sledijo lastnim pravilom.
Ali declare(strict_types=1) pomaga proti ohlapnim primerjavam?
Ne. strict_types nadzoruje, ali se skalarni tipi pri klicih funkcij in vrnjenih vrednostih samodejno pretvarjajo. Na primerjalna operatorja == in != nima vpliva.
Kako pravilno primerjam gesla in žetone?
Gesla preverjate s password_verify, žetone in podpise s hash_equals. Obe primerjata natančno in, pri hash_equals, v konstantnem času. Primerjava z == ali strcmp je za to neprimerna.