Kort gezegd: Voor PHP-unserialize-beveiliging geldt een eenvoudige regel: gegevens die van buiten komen, nooit met unserialize() verwerken. Neem in plaats daarvan JSON. Waar unserialize() uit oude code moet blijven, beperkt u het met allowed_classes => false en controleert u de echtheid van de gegevens met een handtekening.
Wat er bij unserialize() mis kan gaan
PHP kan arrays en objecten met serialize() omzetten in een tekenreeks en met unserialize() herstellen. Dat is handig om gegevens in cookies, caches of databasevelden op te slaan. Het probleem: unserialize() maakt daarbij ook objecten van willekeurige klassen aan, die in de toepassing kunnen worden geladen.
Bij het aanmaken en uiterlijk bij het vernietigen van zo'n object roept PHP automatisch bepaalde methoden aan, zoals __wakeup(), __unserialize() of __destruct(). Bevatten klassen in het project of in bibliotheken zulke methoden met bestands-, database- of systeemacties, dan kan een aanvaller ze via gemanipuleerde gegevens oneigenlijk gebruiken. Vakmensen spreken van PHP object injection. Afhankelijk van de aanwezige klassen loopt het effect uiteen van het wissen van afzonderlijke bestanden tot het uitvoeren van schadelijke code.
Samengevat voor niet-technici: één enkele onbezonnen aanroep op de verkeerde plek kan een aanvaller ver in de toepassing laten. Deze zwakke plek hoort daarom al jaren tot de onderwerpen van de OWASP Top 10.
Het veilige alternatief: JSON in plaats van unserialize
Voor bijna alle gevallen waarin gegevens van buiten komen of de server verlaten, is JSON de juiste keuze. json_decode() maakt alleen arrays, getallen, tekenreeksen en booleaanse waarden aan, geen objecten van eigen klassen.
<?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.');
}
// Alleen bekende velden met verwachte typen overnemen
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
Belangrijk is het tweede deel: ook JSON-gegevens zijn invoer. Neem alleen verwachte velden over en controleer typen en waardebereiken voordat u ermee werkt.
Als unserialize onvermijdelijk is: allowed_classes en handtekening
In oudere toepassingen zitten geserialiseerde gegevens vaak in databases of caches die niet meteen om te zetten zijn. Beperk het risico dan in twee stappen. Ten eerste voorkomt u met allowed_classes dat objecten worden aangemaakt. Ten tweede zorgt u met een HMAC-handtekening ervoor dat de gegevens werkelijk van uw toepassing komen en onderweg niet zijn gewijzigd.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // nooit in de coderepository
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;
}
Zijn bepaalde klassen toch nodig, noem ze dan afzonderlijk in allowed_classes, in plaats van alle toe te staan. De sleutel hoort in de serverconfiguratie of een geheimenopslag en moet na een incident worden vernieuwd.
Bestaande gegevens stapsgewijs naar JSON omzetten
Bij gegroeide toepassingen staan geserialiseerde waarden vaak jarenlang in de database. Een omzetting in één stap is dan riskant. Een overgang heeft zich bewezen: de toepassing leest beide formaten, maar schrijft alleen nog JSON. Bij het lezen van een oude waarde wordt die eenmalig met allowed_classes => false uitgepakt en meteen als JSON teruggeschreven. Na enige tijd zijn de meeste vermeldingen omgezet en een eenmalig script doet de rest.
Log tijdens de overgang hoe vaak nog het oude formaat wordt gelezen, en ook wanneer handtekeningen niet kloppen. Zulke mislukkingen kunnen op manipulatiepogingen wijzen en horen in de monitoring. Pas wanneer er geen oud formaat meer opduikt, verwijdert u de unserialize()-tak volledig uit de code.
Verborgen deserialisatie: sessies, caches en bestandspaden
Niet elke deserialisatie staat zichtbaar in de eigen code. Controleer ook deze plekken:
- Sessies: PHP serialiseert sessiegegevens automatisch. Zolang aanvallers de sessiebestanden niet kunnen schrijven, is dat onkritisch. Gescheiden sessiepaden per website beschrijft de gids Sessies in PHP beveiligen.
- Sessieserializer: De melding over CVE-2026-115560 betreft een ongebruikelijke configuratie waarbij de
serialize_handlertijdens de uitvoering viaini_set()wordt omgezet en session upload progress actief is. Tussenoplossing volgens php.net:session.upload_progress.enabled=0in php.ini of FPM-pool, daarna herstart van de workers. - Caches en wachtrijen: Redis, Memcached of databasetabellen met geserialiseerde waarden zijn maar zo veilig als de toegang ertoe. Ze horen niet open op het netwerk.
- Bestandsfuncties met paden uit invoer: Geef bestandsfuncties nooit paden uit het verzoek. Stream-wrappers als
phar://kunnen anders onverwachte verwerking veroorzaken. Een toegestane lijst voor bestandsnamen beschermt hier betrouwbaarder dan welke filtering ook. - Afhankelijkheden: Bibliotheken met bekende kwetsbaarheden vergroten de mogelijkheden van een aanvaller. Houd ze actueel, zoals beschreven in de gids Composer-afhankelijkheden op kwetsbaarheden controleren.
Riskante plekken in de code vinden
Een eerste inventarisatie lukt met een eenvoudige zoekopdracht. Ze wijzigt niets aan het systeem en toont alle aanroepen buiten de map vendor:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Beoordeel elke treffer met drie vragen: waar komen de gegevens vandaan? Kan een gebruiker ze wijzigen? Is allowed_classes ingesteld? Treffers met invoer uit cookies, $_GET, $_POST, $_REQUEST of externe koppelingen hebben voorrang. Vervang ze door JSON en test de omzetting voordat u ze uitrolt.
Nog een aanknopingspunt ligt in de browser. Geserialiseerde PHP-gegevens hebben een typische opbouw met typeaanduidingen en lengtegegevens. Vindt u zulke waarden in cookies, verborgen formuliervelden of parameters van uw eigen website, dan heeft de toepassing geserialiseerde gegevens aan de bezoeker meegegeven en verwacht ze vermoedelijk terug. Juist deze plekken moeten worden gecontroleerd.
Voor de directie volstaat een eenvoudige vraag aan ontwikkelaar of bureau: „Verwerkt onze toepassing ergens geserialiseerde gegevens die van buiten komen, en zo ja, hoe zijn ze beschermd?” Een duidelijk antwoord met vindplaatsen is een goed teken. Een ontwijkend antwoord is een reden om de toepassing te laten controleren.
Hoe helpt PHP SOS concreet?
- We doorzoeken uw code op deserialisatie met vreemde gegevens en beoordelen elke vondst naar bereikbaarheid en gevolgen.
- We stellen concrete omzettingen naar JSON of beveiligde aanroepen voor en begeleiden uw ontwikkelaars bij de uitvoering.
- We controleren afhankelijkheden op bekende kwetsbaarheden en documenteren de stand.
- Op verzoek laten we de toepassing in het kader van een beveiligingsaudit en penetratietest onafhankelijk controleren.
Veelgestelde vragen
Is unserialize() in PHP principieel onveilig?
Niet voor gegevens die alleen uw eigen toepassing aanmaakt en die niemand kan wijzigen. Gevaarlijk wordt het zodra gebruikers of derden de invoer kunnen beïnvloeden, bijvoorbeeld via cookies, formuliervelden of externe koppelingen.
Volstaat allowed_classes => false als bescherming?
Het voorkomt dat willekeurige objecten worden aangemaakt en sluit daarmee de belangrijkste aanvalsroute. Voor invoer van buiten is JSON toch de betere keuze, omdat de parser eenvoudiger is en geen objecten reconstrueert.
Helpt een update van bibliotheken tegen deserialisatieaanvallen?
Gedeeltelijk. Aanvallen gebruiken vaak klassen uit afhankelijkheden die bij het vernietigen of wekken van een object acties uitvoeren. Actuele pakketten dichten bekende ketens, maar de eigenlijke oorzaak is de unserialize-aanroep met vreemde gegevens.
Heeft PHP2Shell iets met deserialisatie te maken?
De twee hoofdkwetsbaarheden zitten in de verwerking van aanvragen door PHP. De melding over CVE-2026-115560 betreft daarentegen een ongebruikelijke configuratie van de sessieserialisatie. De tussenoplossing daar is session.upload_progress.enabled=0.