Lyhyesti: PHP unserialize -tietoturvassa pätee yksinkertainen sääntö: älkää koskaan käsitelkö ulkopuolelta tulevia tietoja unserialize()-funktiolla. Käyttäkää sen sijaan JSON:ia. Missä unserialize() on vanhasta koodista pakko säilyttää, rajatkaa se asetuksella allowed_classes => false ja tarkistakaa tietojen aitous allekirjoituksella.
Mikä unserialize()-funktiossa voi mennä pieleen
PHP voi muuntaa taulukot ja objektit serialize()-funktiolla merkkijonoksi ja palauttaa ne unserialize()-funktiolla. Se on kätevää, kun tietoja tallennetaan evästeisiin, välimuisteihin tai tietokantakenttiin. Ongelma: unserialize() luo samalla myös mielivaltaisten luokkien objekteja, jotka voidaan ladata sovelluksessa.
Tällaisen objektin luomisen yhteydessä ja viimeistään sen tuhoamisen yhteydessä PHP kutsuu automaattisesti tiettyjä metodeja, kuten __wakeup(), __unserialize() tai __destruct(). Jos projektin tai kirjastojen luokissa on tällaisia metodeja, jotka tekevät tiedosto-, tietokanta- tai järjestelmätoimintoja, hyökkääjä voi käyttää niitä väärin manipuloiduilla tiedoilla. Asiantuntijat puhuvat PHP Object Injectionista. Olemassa olevista luokista riippuen vaikutus ulottuu yksittäisten tiedostojen poistamisesta haittakoodin suorittamiseen.
Ei-teknisille tiivistettynä: yksi harkitsematon kutsu väärässä paikassa voi päästää hyökkääjän syvälle sovellukseen. Siksi tämä heikkous on vuosia kuulunut OWASP Top 10 -aiheisiin.
Turvallinen vaihtoehto: JSON unserializen sijaan
Lähes kaikissa tapauksissa, joissa tiedot tulevat ulkopuolelta tai poistuvat palvelimelta, JSON on oikea valinta. json_decode() luo vain taulukoita, lukuja, merkkijonoja ja totuusarvoja, ei omien luokkien objekteja.
<?php
declare(strict_types=1);
function leseEinstellungen(string $eingabe): array
{
try {
$daten = json_decode($eingabe, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new InvalidArgumentException('Ungültige Daten.');
}
if (!is_array($daten)) {
throw new InvalidArgumentException('Erwartet wurde ein Objekt.');
}
// Ota käyttöön vain tunnetut kentät odotetuilla tyypeillä
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
Tärkeä on toinen osa: myös JSON-tiedot ovat syötteitä. Ottakaa käyttöön vain odotetut kentät ja tarkistakaa tyypit ja arvoalueet, ennen kuin käsittelette niitä.
Kun unserialize on väistämätön: allowed_classes ja allekirjoitus
Vanhemmissa sovelluksissa serialisoituja tietoja on usein tietokannoissa tai välimuisteissa, joita ei voi muuttaa heti. Silloin rajaatte riskiä kahdessa vaiheessa. Ensiksi estätte allowed_classes-asetuksella objektien luomisen. Toiseksi varmistatte HMAC-allekirjoituksella, että tiedot todella tulevat sovelluksestanne eikä niitä ole muutettu matkalla.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // ei koskaan koodirepositoriossa
function verpacken(array $daten): string
{
$roh = serialize($daten);
return hash_hmac('sha256', $roh, SCHLUESSEL) . '.' . base64_encode($roh);
}
function auspacken(string $paket): array
{
[$signatur, $b64] = array_pad(explode('.', $paket, 2), 2, '');
$roh = base64_decode($b64, true);
if ($roh === false || !hash_equals(hash_hmac('sha256', $roh, SCHLUESSEL), $signatur)) {
throw new RuntimeException('Signatur ungültig.');
}
$daten = unserialize($roh, ['allowed_classes' => false, 'max_depth' => 16]);
if (!is_array($daten)) {
throw new RuntimeException('Unerwartetes Format.');
}
return $daten;
}
Jos tiettyjä luokkia kuitenkin tarvitaan, nimetkää ne yksitellen allowed_classes-asetuksessa sen sijaan, että sallisitte kaikki. Avain kuuluu palvelimen asetuksiin tai salaisuuksien tallennukseen, ja se on uusittava poikkeaman jälkeen.
Olemassa olevien tietojen vaiheittainen siirto JSON:iin
Vanhoissa sovelluksissa serialisoituja arvoja on usein tietokannassa vuosien ajalta. Siirto yhdessä vaiheessa on silloin riskialtista. Siirtymävaihe on osoittautunut toimivaksi: sovellus lukee molempia muotoja mutta kirjoittaa enää vain JSON:ia. Vanhaa arvoa luettaessa se puretaan kerran asetuksella allowed_classes => false ja kirjoitetaan heti takaisin JSON:na. Jonkin ajan kuluttua suurin osa merkinnöistä on siirretty, ja kertaluonteinen komentosarja hoitaa loput.
Kirjatkaa siirtymän aikana, kuinka usein vanhaa muotoa vielä luetaan, ja myös silloin, kun allekirjoitukset eivät täsmää. Tällaiset epäonnistumiset voivat viitata manipulointiyrityksiin ja kuuluvat valvontaan. Vasta kun vanhaa muotoa ei enää esiinny, poistakaa unserialize()-haara koodista kokonaan.
Piilotettu deserialisointi: istunnot, välimuistit ja tiedostopolut
Kaikki deserialisointi ei näy omassa koodissa. Tarkistakaa myös nämä kohdat:
- Istunnot: PHP serialisoi istuntotiedot automaattisesti. Niin kauan kuin hyökkääjät eivät voi kirjoittaa istuntotiedostoja, se on vaaratonta. Verkkosivustokohtaiset istuntopolut kuvataan oppaassa Istuntojen suojaaminen PHP:ssä.
- Istuntojen serialisoija: Tiedote CVE-2026-115560:stä koskee epätavallista asetusta, jossa
serialize_handlervaihdetaan ajon aikanaini_set()-komennolla ja Session Upload Progress on aktiivinen. php.netin mukainen keino:session.upload_progress.enabled=0php.ini-tiedostossa tai FPM-poolissa, sen jälkeen worker-prosessien uudelleenkäynnistys. - Välimuistit ja jonot: Redis, Memcached tai tietokantataulut, joissa on serialisoituja arvoja, ovat yhtä turvallisia kuin pääsy niihin. Niitä ei pidä jättää avoimiksi verkkoon.
- Tiedostofunktiot syötteistä tulevilla poluilla: Älkää koskaan antako tiedostofunktioille polkuja pyynnöstä. Stream-wrapperit kuten
phar://voivat muuten käynnistää odottamatonta käsittelyä. Tiedostonimien sallittujen luettelo suojaa tässä luotettavammin kuin mikään suodatus. - Riippuvuudet: Kirjastot, joissa on tunnettuja heikkouksia, laajentavat hyökkääjän mahdollisuuksia. Pitäkää ne ajan tasalla, kuten oppaassa Composer-riippuvuuksien haavoittuvuuksien tarkistus kuvataan.
Riskialttiiden kohtien löytäminen koodista
Ensimmäinen kartoitus onnistuu yksinkertaisella haulla. Se ei muuta järjestelmää mitenkään ja näyttää kaikki kutsut vendor-hakemiston ulkopuolella:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Arvioikaa jokainen osuma kolmella kysymyksellä: mistä tiedot tulevat? Voiko käyttäjä muuttaa niitä? Onko allowed_classes asetettu? Osumat, joissa syötteet tulevat evästeistä, muuttujista $_GET, $_POST, $_REQUEST tai ulkoisista rajapinnoista, ovat etusijalla. Korvatkaa ne JSON:lla ja testatkaa siirtymä, ennen kuin otatte sen käyttöön.
Toinen vihje löytyy selaimesta. Serialisoiduilla PHP-tiedoilla on tyypillinen rakenne, jossa on tyyppitunnisteet ja pituustiedot. Jos löydätte tällaisia arvoja evästeistä, piilotetuista lomakekentistä tai omien verkkosivustojenne parametreista, sovellus on antanut serialisoituja tietoja kävijälle ja odottaa niitä todennäköisesti takaisin. Juuri nämä kohdat on tarkistettava.
Johdolle riittää yksinkertainen kysymys kehittäjille tai toimistolle: ”Käsitteleekö sovelluksemme jossain ulkopuolelta tulevaa serialisoitua dataa, ja jos käsittelee, miten se on suojattu?” Selkeä vastaus löytökohtineen on hyvä merkki. Kiertelevä vastaus on syy teettää sovelluksen tarkistus.
Miten PHP SOS auttaa konkreettisesti?
- Käymme koodinne läpi deserialisoinnin osalta vierailla tiedoilla ja arvioimme jokaisen löydön tavoitettavuuden ja vaikutuksen mukaan.
- Ehdotamme konkreettisia siirtymiä JSON:iin tai suojattuihin kutsuihin ja tuemme kehittäjiänne toteutuksessa.
- Tarkistamme riippuvuudet tunnettujen heikkouksien varalta ja dokumentoimme tilan.
- Halutessanne annamme sovelluksen tarkistuttaa riippumattomasti osana tietoturva-auditointia ja penetraatiotestiä.
Usein kysytyt kysymykset
Onko unserialize() PHP:ssä periaatteessa turvaton?
Ei tiedoille, jotka vain oma sovelluksenne tuottaa ja joita kukaan ei voi muuttaa. Vaaralliseksi se muuttuu heti, kun käyttäjät tai kolmannet osapuolet voivat vaikuttaa syötteeseen, esimerkiksi evästeiden, lomakekenttien tai ulkoisten rajapintojen kautta.
Riittääkö allowed_classes => false suojaksi?
Se estää mielivaltaisten objektien luomisen ja sulkee siten tärkeimmän hyökkäysreitin. Ulkopuolelta tuleville syötteille JSON on silti parempi valinta, koska jäsennin on yksinkertaisempi eikä se rekonstruoi objekteja.
Auttaako kirjastojen päivitys deserialisointihyökkäyksiä vastaan?
Osittain. Hyökkäykset käyttävät usein riippuvuuksien luokkia, jotka suorittavat toimintoja objektia tuhottaessa tai herätettäessä. Ajantasaiset paketit sulkevat tunnetut ketjut, mutta varsinainen syy on unserialize-kutsu vierailla tiedoilla.
Onko PHP2Shellillä jotain tekemistä deserialisoinnin kanssa?
Kaksi pääaukkoa ovat PHP:n pyyntöjen käsittelyssä. CVE-2026-115560:tä koskeva tiedote sen sijaan liittyy istuntojen serialisoinnin epätavalliseen asetukseen. Keino on siinä session.upload_progress.enabled=0.