Lyhyesti: PHP Type Juggling tarkoittaa automaattista tyyppimuunnosta, joka ratkaisee tuloksen löyhässä vertailussa operaattorilla ==. PHP 8.0:sta alkaen säännöt ovat tiukempia, mutta numeerisia merkkijonoja vertaillaan yhä lukuina, ja true, false ja null noudattavat omia sääntöjään. Tietoturvan kannalta olennaisissa tarkistuksissa pätee siksi: ===, in_array(..., true), match switchin sijaan, hash_equals() tokeneille ja password_verify() salasanoille.
Vertailu, joka on tosi, vaikka sen ei pitäisi olla
Monet PHP-sovellusten tietoturva-aukot eivät piile monimutkaisissa funktioissa vaan yhdessä merkissä: == siellä, minne olisi kuulunut ===. Koodi näyttää oikealta, testit tavallisilla syötteillä menevät läpi, ja silti tarkistus päästää tietyissä olosuhteissa läpi jotain, minkä sen pitäisi pysäyttää.
Syy on PHP:n ominaisuus, joka on arjessa käytännöllinen: arvot muunnetaan tarvittaessa toiseen tyyppiin. Lomakkeen merkkijonoa voidaan siten vertailla suoraan tietokannan lukuun. Kirjautumisissa, tokeneissa, rooleissa tai tilakentissä juuri tämä sallivuus voi olla vaarallista.
Miten PHP vertaa operaattorilla ==
PHP-käsikirja kuvaa löyhien vertailujen säännöt tarkasti. Tietoturvan kannalta tärkeimpiä ovat nämä tapaukset:
| Vertailu | PHP 7 | PHP 8 | Syy |
|---|---|---|---|
0 == "a" |
tosi | epätosi | PHP 8.0:sta alkaen vertaillaan merkkijonoina |
"1" == "01" |
tosi | tosi | molemmat numeerisia, vertailu lukuna |
"10" == "1e1" |
tosi | tosi | eksponenttimuoto on numeerinen |
100 == "1e2" |
tosi | tosi | luku numeerista merkkijonoa vastaan |
null == false |
tosi | tosi | vertailu totuusarvona |
"abc" == true |
tosi | tosi | ei-tyhjä merkkijono katsotaan todeksi |
PHP 8.0:n muutos lievensi kokonaisen virheluokan: aiemmin ei-numeerinen merkkijono muuttui vertailussa lukuun nähden arvoksi 0. Tämä koski myös switch-lauseita. Taulukon muut rivit osoittavat kuitenkin, että löyhät vertailut pitävät yhä sisällään yllätyksiä.
Myös operaattorit <, > ja vertailuoperaattori <=> noudattavat näitä sääntöjä. Lajitteluissa se on yleensä harmitonta, mutta tarkistuksissa kuten ”summa pienempi kuin raja” pyynnöstä tulevilla arvoilla tyypin pitäisi olla ennalta selvä.
Erityisen salakavala on eksponenttimuotoa koskeva rivi. Tiivisteet ja tokenit koostuvat usein numeroista ja kirjaimista. Jos tällainen arvo näyttää sattumalta eksponenttimuotoiselta luvulta, == saattaa vertailla kahta eri tiivistettä yhtäläisinä. Tämä on tausta monille raporteille kirjautumisen ohituksista vanhemmissa PHP-sovelluksissa.
PHP Type Juggling tyypillisissä tietoturvatarkistuksissa
Löyhät vertailut piilevät useammassa kohdassa kuin pelkässä operaattorissa ==:
in_array()jaarray_search()vertaavat ilman kolmatta parametria löyhästi. Rooleja tai tunnisteita sisältävä sallittujen luettelo on siten pehmeämpi kuin luulisi.switchvertaa löyhästi. PHP 8.0:sta saatavilla oleva lausekematchvertaa sen sijaan käsikirjan mukaan tyyppiturvallisesti operaattorilla===ja heittää virheen, jos mikään haara ei sovi.- JSON-syötteet tuottavat todellisia tyyppejä. Kenttä, jonka odotatte olevan merkkijono, voi saapua arvona
truetai lukuna ja täsmätä yllättäen operaattorilla==. strcmp()salaisuuksille ei sovellu, koska ajoaika riippuu sisällöstä ja virhetapaukset arvioidaan helposti väärin.- Paluuarvojen tarkistus: Funktiot kuten
strpos()palauttavat0osumasta alussa jafalse, jos osumaa ei ole.if (!strpos(...))sekoittaa nämä.
Turvalliset vertailut: vastakkainasettelu
Seuraavat esimerkit näyttävät kussakin turvattoman ja turvallisen version:
<?php
declare(strict_types=1);
// Tarkista rooli
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // turvaton: löyhä
in_array($rolle, $erlaubteRollen, true); // turvallinen: tiukka
// Arvioi tila
$aktion = match ($status) { // tiukka, virhe tuntemattomalla arvolla
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Tarkista linkin token
if ($tokenAusLink == $gespeichertesToken) { /* turvaton */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* turvallinen */ }
// Tarkista salasana
if (md5($eingabe) == $hash) { /* turvaton: heikko menetelmä ja löyhä vertailu */ }
if (password_verify($eingabe, $hash)) { /* turvallinen */ }
// Etsi osamerkkijono
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
hash_equals() odottaa käsikirjan mukaan tunnetun salaisuuden ensimmäisenä ja käyttäjän arvon toisena parametrina ja vertaa vakioajassa. Miten tokenit ylipäätään luodaan turvallisesti, kuvaa Turvalliset satunnaisluvut ja tokenit PHP:ssä.
declare(strict_types=1) ei tee vertailuista tiukkoja. Lause koskee vain tyyppimuunnosta funktiokutsuissa ja paluuarvoissa. Löyhät vertailut teidän on korvattava koodissa itse.
Tiukoilla vertailuilla on kääntöpuolensa: ne epäonnistuvat, jos tyypit eivät täsmää. '42' === 42 on epätosi. Lomakkeiden ja URL-osoitteiden arvot saapuvat PHP:ssä merkkijonoina, tietokannan arvot ajurista ja asetuksista riippuen merkkijonoina tai lukuina. Joka ottaa kaikkialla käyttöön === yhdenmukaistamatta tyyppejä, aiheuttaa virheitä, jotka huomataan käytössä oikeutettujen käyttäjien hylkäämisenä.
Ratkaisu on selkeä raja: syötteet validoidaan vastaanotettaessa ja muunnetaan oikeaan tyyppiin, esimerkiksi filter_var($wert, FILTER_VALIDATE_INT), ja käsitellään sen jälkeen koodissa vain tyyppimäärittelyillä ja tiukoilla vertailuilla. Tyyppimäärittelyt parametreissa, paluuarvoissa ja ominaisuuksissa auttavat tässä, koska väärät tyypit paljastuvat silloin varhain virheilmoituksella sen sijaan, että ne katoaisivat hiljaa vertailuun.
Vanhan koodin järjestelmällinen tarkistus
Vuosien mittaan kasvaneissa projekteissa löyhiä vertailuja on satoja. Kaikki eivät ole ongelma. Keskittykää kohtiin, joissa tehdään päätös pääsystä:
# Löyhät vertailut riveillä, joilla on tietoturvaan liittyviä termejä
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array ilman tiukkaa vertailua
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Staattisen analyysin työkalut kuten PHPStan tai Psalm voivat halutessa ilmoittaa löyhistä vertailuista pysyvästi. Näin uudet tapaukset eivät pääse edes pääkehityshaaraan. Tyyppien perusteet kuvaa PHP:n tyyppijärjestelmä; miten syötteet saatetaan ensin oikeaan tyyppiin, näyttää Syötteiden validointi ja suodatus PHP:ssä.
Mitä tämä tarkoittaa vanhoille järjestelmille
Sovellukset, jotka toimivat yhä PHP 7:llä, ovat vanhojen, löyhempien vertailusääntöjen kohteena. Lisäksi on toinen ongelma: PHP 8.1:lle ja sitä vanhemmille PHP-projekti ei enää julkaise virallisia tietoturvapäivityksiä, ei myöskään PHP2Shelliä vastaan. Tilanteen kuvaa PHP 8.1 ja vanhemmat: mikä nyt pätee.
PHP 8:aan migroitaessa jotkin vertailutulokset muuttuvat. Se on yleensä parannus, mutta voi rikkoa koodin, joka on luottanut vanhaan käyttäytymiseen. Suunnitelkaa siksi testit, jotka kattavat kohdennetusti vertailut luvuilla, tyhjillä arvoilla ja arvolla null.
Miten PHP SOS auttaa konkreettisesti?
- Käymme koodinne läpi löyhien vertailujen varalta kohdissa, jotka ratkaisevat kirjautumisesta, oikeuksista tai maksuista.
- Korvaamme ne tiukoilla vertailuilla,
match-lausekkeilla ja sopivilla tarkistusfunktioilla. - Otamme staattisen analyysin käyttöön niin, että uudet löyhät vertailut havaitaan buildissa.
- Tuemme migraatioita ajantasaisiin PHP-versioihin testeillä muuttuneille vertailusäännöille.
Ohjelmistonne tarkistuksen kuvaa tietoturva-auditointi ja tunkeutumistesti.
Usein kysytyt kysymykset
Mitä on Type Juggling PHP:ssä?
PHP muuntaa eri tyyppisiä arvoja automaattisesti joissakin operaatioissa, esimerkiksi merkkijonon luvuksi. Löyhässä vertailussa operaattorilla == tämä muunnos ratkaisee tuloksen. Se on kätevää, mutta voi johtaa vertailuihin, jotka ovat tosia, vaikka arvot eivät näytä samoilta.
Onko PHP 8 ratkaissut ongelman?
Osittain. PHP 8.0:sta alkaen luku ja ei-numeerinen merkkijono vertaillaan merkkijonoina, joten 0 == 'a' ei ole enää tosi. Kaksi numeerista merkkijonoa vertaillaan kuitenkin edelleen lukuina, ja vertailuihin arvoilla true, false ja null pätevät omat säännöt.
Auttaako declare(strict_types=1) löyhiä vertailuja vastaan?
Ei. strict_types ohjaa, muunnetaanko skalaarityypit automaattisesti funktiokutsuissa ja paluuarvoissa. Vertailuoperaattoreihin == ja != sillä ei ole vaikutusta.
Miten vertailen salasanoja ja tokeneita oikein?
Salasanat tarkistatte funktiolla password_verify, tokenit ja allekirjoitukset funktiolla hash_equals. Molemmat vertaavat täsmällisesti ja hash_equals vakioajassa. Vertailu operaattorilla == tai funktiolla strcmp ei sovellu tähän.