Накратко: При сигурността на unserialize във PHP важи просто правило: данни, които идват отвън, никога не обработвайте с unserialize(). Вземете вместо това JSON. Където unserialize() от стар код трябва да остане, ограничете го с allowed_classes => false и проверявайте автентичността на данните с подпис.
Какво може да се обърка при unserialize()
PHP може да превръща масиви и обекти със serialize() в низ и да ги възстановява с unserialize(). Това е удобно за съхраняване на данни в бисквитки, кешове или полета на база данни. Проблемът: unserialize() създава при това и обекти на произволни класове, които могат да бъдат заредени в приложението.
При създаването и най-късно при унищожаването на такъв обект PHP автоматично извиква определени методи, например __wakeup(), __unserialize() или __destruct(). Ако класове в проекта или в библиотеките съдържат такива методи с действия върху файлове, база данни или система, атакуващ може да ги използва не по предназначение чрез манипулирани данни. Специалистите говорят за PHP Object Injection. В зависимост от наличните класове ефектът варира от изтриване на отделни файлове до изпълнение на злонамерен код.
Обобщено за нетехнически лица: едно-единствено необмислено извикване на грешното място може да допусне атакуващ дълбоко в приложението. Затова тази слабост от години е сред темите на OWASP Top 10.
Сигурната алтернатива: JSON вместо unserialize
За почти всички случаи, в които данните идват отвън или напускат сървъра, JSON е правилният избор. json_decode() създава само масиви, числа, низове и булеви стойности, но не и обекти на собствени класове.
<?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.');
}
// Приемат се само известни полета с очаквани типове
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
Важна е втората част: и JSON данните са въвеждане. Приемайте само очакваните полета и проверявайте типовете и диапазоните на стойностите, преди да работите с тях.
Когато unserialize е неизбежна: allowed_classes и подпис
В по-стари приложения сериализираните данни често са в бази данни или кешове, които не могат веднага да се преустроят. Тогава ограничавате риска на две стъпки. Първо, с allowed_classes предотвратявате създаването на обекти. Второ, с HMAC подпис гарантирате, че данните наистина идват от Вашето приложение и не са променени по пътя.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // никога в хранилището на кода
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;
}
Ако все пак са нужни определени класове, назовете ги поотделно в allowed_classes, вместо да допускате всички. Ключът принадлежи в конфигурацията на сървъра или в хранилище за тайни и след инцидент трябва да се подмени.
Постепенно преминаване на съществуващите данни към JSON
В нараснали приложения сериализирани стойности често лежат с години в базата данни. Преминаване с една стъпка тогава е рисковано. Доказа се преход: приложението чете двата формата, но записва само JSON. При четене на стара стойност тя се разопаковва еднократно с allowed_classes => false и веднага се записва обратно като JSON. След известно време повечето записи са преустроени, а еднократен скрипт довършва останалото.
Протоколирайте по време на прехода колко често още се чете старият формат, както и когато подписите не съвпадат. Такива неуспехи могат да сочат към опити за манипулация и принадлежат към наблюдението. Едва когато вече не се появява стар формат, премахнете клона с unserialize() изцяло от кода.
Скрита десериализация: сесии, кешове и пътища до файлове
Не всяка десериализация е видима в собствения код. Проверете и тези места:
- Сесии: PHP сериализира данните на сесиите автоматично. Докато атакуващите не могат да записват файловете на сесиите, това е некритично. Разделени пътища за сесиите за всеки уебсайт описва наръчникът Защита на сесиите в PHP.
- Сериализатор на сесиите: Съобщението за CVE-2026-115560 засяга необичайна конфигурация, при която
serialize_handlerсе превключва по време на изпълнение чрезini_set(), а upload progress на сесиите е активен. Мярка според php.net:session.upload_progress.enabled=0в php.ini или FPM пула, след това рестарт на worker процесите. - Кешове и опашки: Redis, Memcached или таблици на база данни със сериализирани стойности са толкова сигурни, колкото е достъпът до тях. Те не принадлежат отворени в мрежата.
- Файлови функции с пътища от въведени данни: Никога не предавайте на файлови функции пътища от заявката. Stream wrapper-и като
phar://иначе могат да предизвикат неочаквана обработка. Списък с разрешени имена на файлове защитава тук по-надеждно от всяко филтриране. - Зависимости: Библиотеки с известни слабости разширяват възможностите на атакуващия. Поддържайте ги актуални, както е описано в наръчника Проверка на Composer зависимостите за уязвимости.
Откриване на рискови места в кода
Първа инвентаризация се получава с просто търсене. То не променя нищо в системата и показва всички извиквания извън директорията vendor:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Оценете всяко съвпадение с три въпроса: откъде идват данните? Може ли потребител да ги промени? Зададена ли е allowed_classes? Съвпаденията с данни от бисквитки, $_GET, $_POST, $_REQUEST или външни интерфейси имат предимство. Заменете ги с JSON и тествайте преустройството, преди да го внедрите.
Друга опорна точка е в браузъра. Сериализираните PHP данни имат типична структура с идентификатори на типове и данни за дължина. Ако намерите такива стойности в бисквитки, скрити полета на формуляри или параметри на Вашия собствен уебсайт, приложението е предало сериализирани данни на посетителя и вероятно ги очаква обратно. Именно тези места трябва да се проверят.
За ръководството е достатъчен един прост въпрос към разработчиците или агенцията: „Обработва ли нашето приложение някъде сериализирани данни, които идват отвън, и ако да, как са защитени?“ Ясен отговор с конкретни места е добър знак. Уклончив отговор е причина да се поиска проверка на приложението.
Как конкретно помага PHP SOS?
- Претърсваме Вашия код за десериализация с чужди данни и оценяваме всяка находка според достъпността и въздействието.
- Предлагаме конкретни преустройства към JSON или обезопасени извиквания и подкрепяме Вашите разработчици при реализацията.
- Проверяваме зависимостите за известни слабости и документираме състоянието.
- При желание организираме независима проверка на приложението в рамките на одит на сигурността и тест за проникване.
Често задавани въпроси
Принципно несигурна ли е unserialize() във PHP?
Не за данни, които се генерират само от Вашето приложение и които никой не може да промени. Опасно става щом потребители или трети лица могат да влияят на въведеното, например чрез бисквитки, полета на формуляри или външни интерфейси.
Достатъчно ли е allowed_classes => false като защита?
Така се предотвратява създаването на произволни обекти и се затваря най-важният път за атака. За входни данни отвън JSON все пак е по-добрият избор, защото парсерът е по-прост и не възстановява обекти.
Помага ли актуализацията на библиотеките срещу атаки чрез десериализация?
Отчасти. Атаките често използват класове от зависимости, които при унищожаване или събуждане на обект изпълняват действия. Актуалните пакети затварят известни вериги, но истинската причина е извикването на unserialize с чужди данни.
Има ли PHP2Shell нещо общо с десериализацията?
Двете основни уязвимости са в обработката на заявките на PHP. Съобщението за CVE-2026-115560 пък засяга необичайна конфигурация на сериализацията на сесиите. Мярката там е session.upload_progress.enabled=0.