Trumpai: PHP unserialize saugai galioja paprasta taisyklė: duomenų, kurie ateina iš išorės, niekada neapdoroti su unserialize(). Vietoj to naudokite JSON. Kur unserialize() iš senojo kodo turi likti, apribokite jį su allowed_classes => false ir duomenų autentiškumą tikrinkite parašu.
Kas gali nepasisekti su unserialize()
PHP gali masyvus ir objektus su serialize() paversti simbolių eilute ir su unserialize() atkurti. Tai patogu duomenims laikyti slapukuose, talpyklose ar duomenų bazės laukuose. Problema ta, kad unserialize() kartu sukuria ir savavališkų klasių objektus, kurios gali būti įkeltos programoje.
Kuriant ir bent jau naikinant tokį objektą PHP automatiškai iškviečia tam tikrus metodus, pavyzdžiui, __wakeup(), __unserialize() ar __destruct(). Jei projekto ar bibliotekų klasės tokius metodus turi su failų, duomenų bazės ar sistemos veiksmais, užpuolikas per suklastotus duomenis gali jais piktnaudžiauti. Specialistai kalba apie PHP Object Injection. Priklausomai nuo esamų klasių, poveikis siekia nuo atskirų failų ištrynimo iki kenkėjiško kodo vykdymo.
Ne technikams apibendrinus: vienas neapgalvotas iškvietimas netinkamoje vietoje gali užpuoliką įleisti giliai į programą. Todėl šis pažeidžiamumas jau daug metų priklauso OWASP Top 10 temoms.
Saugi alternatyva: JSON vietoj unserialize
Beveik visais atvejais, kai duomenys ateina iš išorės arba palieka serverį, JSON yra teisingas pasirinkimas. json_decode() sukuria tik masyvus, skaičius, simbolių eilutes ir loginės reikšmes, jokių nuosavų klasių objektų.
<?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.');
}
// Perimti tik žinomus laukus su tikėtinais tipais
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
Svarbi antra dalis: JSON duomenys taip pat yra įvestys. Perimkite tik tikėtinus laukus ir prieš naudodami patikrinkite tipus bei reikšmių intervalus.
Kai unserialize neišvengiamas: allowed_classes ir parašas
Senesnėse programose serializuotos reikšmės dažnai slypi duomenų bazėse ar talpyklose, kurių iš karto pakeisti neįmanoma. Tuomet riziką apribokite dviem žingsniais. Pirma, su allowed_classes neleiskite kurti objektų. Antra, su HMAC parašu užtikrinkite, kad duomenys tikrai atėjo iš jūsų programos ir pakeliui nebuvo pakeisti.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // niekada ne kodo saugykloje
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;
}
Jei vis dėlto reikia tam tikrų klasių, įvardykite jas po vieną allowed_classes, užuot leidę visas. Raktas priklauso serverio konfigūracijai ar paslapčių saugyklai ir po incidento turi būti pakeistas.
Esamų duomenų palaipsnis perkėlimas į JSON
Išaugusiose programose serializuotos reikšmės dažnai metų metais guli duomenų bazėje. Perėjimas vienu žingsniu tuomet rizikingas. Pasiteisino pereinamasis laikotarpis: programa skaito abu formatus, bet rašo tik JSON. Perskaičius seną reikšmę, ji vieną kartą išpakuojama su allowed_classes => false ir iškart įrašoma atgal kaip JSON. Po kurio laiko dauguma įrašų perkelti, o likusius sutvarko vienkartinis skriptas.
Pereinamuoju laikotarpiu registruokite, kaip dažnai dar skaitomas senas formatas, taip pat kai parašai nesutampa. Tokios nesėkmės gali rodyti manipuliavimo bandymus ir priklauso stebėsenai. Tik kai senas formatas nebepasirodo, unserialize() šaką visiškai pašalinkite iš kodo.
Paslėpta deserializacija: sesijos, talpyklos ir failų keliai
Ne kiekviena deserializacija matoma jūsų kode. Patikrinkite ir šias vietas:
- Sesijos: PHP sesijų duomenis serializuoja automatiškai. Kol užpuolikai negali rašyti sesijų failų, tai nekritiška. Atskirus sesijų kelius kiekvienai svetainei aprašo vadovas PHP sesijų apsauga.
- Sesijų serializatorius: Pranešimas apie CVE-2026-115560 liečia neįprastą konfigūraciją, kai
serialize_handlervykdymo metu perjungiamas perini_set(), o session upload progress yra aktyvus. Pagalba pagal php.net:session.upload_progress.enabled=0php.ini ar FPM baseine, po to darbinių procesų paleidimas iš naujo. - Talpyklos ir eilės: Redis, Memcached ar duomenų bazės lentelės su serializuotomis reikšmėmis yra tiek saugios, kiek saugi prieiga prie jų. Jos neturi būti atviros tinkle.
- Failų funkcijos su keliais iš įvesties: Failų funkcijoms niekada neperduokite kelių iš užklausos. Kitaip srautų apvalkalai (stream wrapper), tokie kaip
phar://, gali sukelti netikėtą apdorojimą. Leistinų failų pavadinimų sąrašas čia saugo patikimiau nei bet koks filtravimas. - Priklausomybės: Bibliotekos su žinomais pažeidžiamumais plečia užpuoliko galimybes. Laikykite jas atnaujintas, kaip aprašyta vadove Composer priklausomybių patikra dėl spragų.
Rizikingų vietų paieška kode
Pirmą inventorizaciją galima atlikti paprasta paieška. Ji nieko nekeičia sistemoje ir parodo visus iškvietimus už vendor katalogo ribų:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Kiekvieną radinį įvertinkite trimis klausimais: iš kur duomenys? Ar naudotojas gali juos pakeisti? Ar nustatyta allowed_classes? Pirmenybę turi radiniai su įvestimis iš slapukų, $_GET, $_POST, $_REQUEST ar išorinių sąsajų. Pakeiskite juos JSON ir prieš diegdami išbandykite perėjimą.
Dar viena užuomina yra naršyklėje. Serializuoti PHP duomenys turi tipišką struktūrą su tipų žymėjimais ir ilgio nuorodomis. Jei tokių reikšmių randate savo svetainės slapukuose, paslėptuose formų laukuose ar parametruose, programa serializuotus duomenis atidavė lankytojui ir greičiausiai tikisi juos gauti atgal. Būtent šias vietas reikia tikrinti.
Vadovybei pakanka paprasto klausimo kūrėjams ar agentūrai: „Ar mūsų programa kur nors apdoroja serializuotus duomenis, ateinančius iš išorės, ir jei taip, kaip jie apsaugoti?“ Aiškus atsakymas su radimo vietomis yra geras ženklas. Išsisukinėjantis atsakymas yra priežastis programą patikrinti.
Kaip PHP SOS padeda konkrečiai?
- Ieškome jūsų kode deserializacijos su svetimais duomenimis ir kiekvieną radinį vertiname pagal pasiekiamumą ir poveikį.
- Siūlome konkrečius perėjimus prie JSON ar apsaugotų iškvietimų ir lydime jūsų kūrėjus įgyvendinant.
- Tikriname priklausomybes dėl žinomų pažeidžiamumų ir dokumentuojame būseną.
- Pageidaujant programą nepriklausomai patikriname per saugumo auditą ir skverbties testą.
Dažni klausimai
Ar unserialize() PHP iš principo nesaugus?
Ne duomenims, kuriuos kuria tik jūsų pačių programa ir kurių niekas negali pakeisti. Pavojinga tampa, kai tik naudotojai ar trečiosios šalys gali paveikti įvestį, pavyzdžiui, per slapukus, formų laukus ar išorines sąsajas.
Ar allowed_classes => false kaip apsauga pakanka?
Jis neleidžia sukurti savavališkų objektų ir taip uždaro svarbiausią atakos kelią. Išorinėms įvestims JSON vis tiek yra geresnis pasirinkimas, nes analizatorius paprastesnis ir objektų neatkuria.
Ar bibliotekų atnaujinimas padeda nuo deserializacijos atakų?
Iš dalies. Atakos dažnai naudoja priklausomybių klases, kurios sunaikinant ar pažadinant objektą atlieka veiksmus. Naujausi paketai uždaro žinomas grandines, tačiau tikroji priežastis yra unserialize iškvietimas su svetimais duomenimis.
Ar PHP2Shell turi ką nors bendra su deserializacija?
Du pagrindiniai pažeidžiamumai yra PHP užklausų apdorojime. Pranešimas apie CVE-2026-115560, priešingai, liečia neįprastą sesijų serializacijos konfigūraciją. Pagalba ten yra session.upload_progress.enabled=0.