# Type Juggling PHP:ssä: löyhät vertailut tietoturvariskinä

> PHP Type Jugglingin ymmärtäminen: miten == vertaa, mitä PHP 8.0 muutti ja miksi tokenit, tiivisteet ja roolit on aina vertailtava tiukasti.

Lähde: https://phpsos.de/fi/blog/type-juggling-php/  
Tilanne: 25.10.2026  
Palveluntarjoaja: PHP SOS, PHP-hätäapu ja palvelimen tietoturva. Pikaapu WhatsAppilla: https://wa.me/491736494914?text=Hei%20PHP%20SOS%2C%20tarvitsemme%20apua%20PHP-palvelimemme%20kanssa.%20Yritys%3A%20%E2%80%A6%20Verkkosivusto%3A%20%E2%80%A6

**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` `switch`in 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 `==`:

1. **`in_array()` ja `array_search()`** vertaavat ilman kolmatta parametria löyhästi. Rooleja tai tunnisteita sisältävä sallittujen luettelo on siten pehmeämpi kuin luulisi.
2. **`switch`** vertaa löyhästi. PHP 8.0:sta saatavilla oleva lauseke `match` vertaa sen sijaan käsikirjan mukaan tyyppiturvallisesti operaattorilla `===` ja heittää virheen, jos mikään haara ei sovi.
3. **JSON-syötteet** tuottavat todellisia tyyppejä. Kenttä, jonka odotatte olevan merkkijono, voi saapua arvona `true` tai lukuna ja täsmätä yllättäen operaattorilla `==`.
4. **`strcmp()` salaisuuksille** ei sovellu, koska ajoaika riippuu sisällöstä ja virhetapaukset arvioidaan helposti väärin.
5. **Paluuarvojen tarkistus**: Funktiot kuten `strpos()` palauttavat `0` osumasta alussa ja `false`, jos osumaa ei ole. `if (!strpos(...))` sekoittaa nämä.

## Turvalliset vertailut: vastakkainasettelu

Seuraavat esimerkit näyttävät kussakin turvattoman ja turvallisen version:

```php
<?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ä](https://phpsos.de/fi/blog/sichere-tokens-php/).

> **Tärkeää:**
> `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ä:

```bash
# 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ä](https://phpsos.de/fi/php/typsystem/); miten syötteet saatetaan ensin oikeaan tyyppiin, näyttää [Syötteiden validointi ja suodatus PHP:ssä](https://phpsos.de/fi/blog/eingaben-validieren-php/).

## 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](https://phpsos.de/fi/php2shell/php-8-1-und-aelter/).

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](https://phpsos.de/fi/leistungen/sicherheitsaudit-penetrationstest/).


## 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.

## Lähteet

- [PHP-käsikirja: vertailuoperaattorit](https://www.php.net/manual/de/language.operators.comparison.php)
- [PHP-käsikirja: match](https://www.php.net/manual/de/control-structures.match.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
