Krótko: W bezpieczeństwie PHP unserialize obowiązuje prosta zasada: danych pochodzących z zewnątrz nigdy nie przetwarzać przez unserialize(). Proszę użyć zamiast tego JSON. Tam, gdzie unserialize() musi pozostać ze starego kodu, proszę ograniczyć je przez allowed_classes => false i sprawdzać autentyczność danych podpisem.
Co może pójść źle przy unserialize()
PHP potrafi zamienić tablice i obiekty za pomocą serialize() na ciąg znaków i odtworzyć je za pomocą unserialize(). Jest to wygodne do zapisywania danych w ciasteczkach, pamięciach podręcznych lub polach bazy danych. Problem: unserialize() tworzy przy tym także obiekty dowolnych klas, które mogą być załadowane w aplikacji.
Przy tworzeniu, a najpóźniej przy niszczeniu takiego obiektu PHP automatycznie wywołuje określone metody, takie jak __wakeup(), __unserialize() lub __destruct(). Jeśli klasy w projekcie lub w bibliotekach mają takie metody z akcjami na plikach, bazie danych lub systemie, atakujący może je wykorzystać niezgodnie z przeznaczeniem przez zmanipulowane dane. Specjaliści mówią o PHP Object Injection. W zależności od istniejących klas skutki sięgają od usunięcia pojedynczych plików po wykonanie złośliwego kodu.
Dla osób nietechnicznych w skrócie: jedno nieprzemyślane wywołanie w niewłaściwym miejscu może wpuścić atakującego głęboko do aplikacji. Ta podatność od lat należy dlatego do tematów OWASP Top 10.
Bezpieczna alternatywa: JSON zamiast unserialize
W niemal wszystkich przypadkach, gdy dane przychodzą z zewnątrz lub opuszczają serwer, JSON jest właściwym wyborem. json_decode() tworzy tylko tablice, liczby, ciągi znaków i wartości logiczne, bez obiektów własnych klas.
<?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.');
}
// Przejmować tylko znane pola z oczekiwanymi typami
return [
'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
'seiten' => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
];
}
Ważna jest druga część: dane JSON również są danymi wejściowymi. Proszę przejmować tylko oczekiwane pola i sprawdzać typy oraz zakresy wartości, zanim zacznie się z nimi pracować.
Gdy unserialize jest nieunikniony: allowed_classes i podpis
W starszych aplikacjach zserializowane dane często tkwią w bazach danych lub pamięciach podręcznych, których nie da się od razu przestawić. Wtedy ryzyko ogranicza się w dwóch krokach. Po pierwsze, za pomocą allowed_classes zapobiega się tworzeniu obiektów. Po drugie, podpisem HMAC upewnia się, że dane faktycznie pochodzą z Państwa aplikacji i nie zostały po drodze zmienione.
<?php
declare(strict_types=1);
const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // nigdy w repozytorium kodu
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;
}
Jeśli jednak potrzebne są określone klasy, proszę wymienić je pojedynczo w allowed_classes, zamiast dopuszczać wszystkie. Klucz należy do konfiguracji serwera lub magazynu sekretów i po incydencie musi zostać odnowiony.
Stopniowe przestawianie istniejących danych na JSON
W rozwijanych latami aplikacjach zserializowane wartości często leżą w bazie danych przez lata. Przestawienie w jednym kroku jest wtedy ryzykowne. Sprawdziło się przejście: aplikacja czyta oba formaty, ale zapisuje już tylko JSON. Przy odczycie starej wartości jest ona jednorazowo rozpakowywana z allowed_classes => false i od razu zapisywana z powrotem jako JSON. Po pewnym czasie większość wpisów jest przestawiona, a resztę załatwia jednorazowy skrypt.
W czasie przejścia proszę protokołować, jak często nadal czytany jest stary format, a także gdy podpisy się nie zgadzają. Takie niepowodzenia mogą wskazywać na próby manipulacji i należą do monitoringu. Dopiero gdy stary format już się nie pojawia, proszę całkowicie usunąć gałąź unserialize() z kodu.
Ukryta deserializacja: sesje, pamięci podręczne i ścieżki plików
Nie każda deserializacja jest widoczna we własnym kodzie. Proszę sprawdzić także te miejsca:
- Sesje: PHP serializuje dane sesji automatycznie. Dopóki atakujący nie mogą zapisywać plików sesji, jest to niekrytyczne. Oddzielne ścieżki sesji dla każdej strony opisuje poradnik Zabezpieczanie sesji w PHP.
- Serializery sesji: Komunikat o CVE-2026-115560 dotyczy nietypowej konfiguracji, w której
serialize_handlerjest przestawiany w czasie działania przezini_set(), a session upload progress jest aktywny. Środek zaradczy według php.net:session.upload_progress.enabled=0w php.ini lub puli FPM, potem restart procesów roboczych. - Pamięci podręczne i kolejki: Redis, Memcached lub tabele bazy danych z zserializowanymi wartościami są tak bezpieczne, jak dostęp do nich. Nie należą otwarcie do sieci.
- Funkcje plikowe ze ścieżkami z danych wejściowych: funkcjom plikowym nigdy nie przekazywać ścieżek z żądania. Wrappery strumieni, takie jak
phar://, mogłyby w przeciwnym razie wywołać nieoczekiwane przetwarzanie. Lista dozwolonych nazw plików chroni tu bardziej niezawodnie niż jakiekolwiek filtrowanie. - Zależności: biblioteki ze znanymi podatnościami rozszerzają możliwości atakującego. Proszę utrzymywać je w aktualnym stanie, jak opisuje poradnik composer audit: kontrola zależności pod kątem luk.
Wyszukiwanie ryzykownych miejsc w kodzie
Pierwszą inwentaryzację umożliwia proste wyszukiwanie. Niczego nie zmienia w systemie i pokazuje wszystkie wywołania poza katalogiem vendor:
grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
| grep -v '/vendor/'
Każde trafienie proszę ocenić trzema pytaniami: skąd pochodzą dane? Czy użytkownik może je zmienić? Czy ustawiono allowed_classes? Trafienia z danymi z ciasteczek, $_GET, $_POST, $_REQUEST lub zewnętrznych interfejsów mają pierwszeństwo. Proszę zastąpić je JSON-em i przetestować przestawienie, zanim zostanie wdrożone.
Kolejna wskazówka leży w przeglądarce. Zserializowane dane PHP mają typową budowę z oznaczeniami typów i podaniem długości. Jeśli znajdą Państwo takie wartości w ciasteczkach, ukrytych polach formularzy lub parametrach własnej strony, aplikacja przekazała zserializowane dane odwiedzającemu i prawdopodobnie oczekuje ich z powrotem. Właśnie te miejsca należy sprawdzić.
Dla zarządu wystarczy proste pytanie do programistów lub agencji: „Czy nasza aplikacja gdzieś przetwarza zserializowane dane pochodzące z zewnątrz, a jeśli tak, jak są chronione?” Jasna odpowiedź ze wskazaniem miejsc to dobry znak. Wymijająca odpowiedź jest powodem, by zlecić kontrolę aplikacji.
Jak konkretnie pomaga PHP SOS?
- Przeszukujemy Państwa kod pod kątem deserializacji z obcymi danymi i oceniamy każde znalezisko według osiągalności i skutków.
- Proponujemy konkretne przejścia na JSON lub zabezpieczone wywołania i wspieramy Państwa programistów we wdrożeniu.
- Sprawdzamy zależności pod kątem znanych podatności i dokumentujemy stan.
- Na życzenie zlecamy niezależną kontrolę aplikacji w ramach audytu bezpieczeństwa i testu penetracyjnego.
Częste pytania
Czy unserialize() w PHP jest zasadniczo niebezpieczne?
Nie dla danych, które tworzy wyłącznie Państwa aplikacja i których nikt nie może zmienić. Niebezpiecznie robi się, gdy tylko użytkownicy lub osoby trzecie mogą wpływać na dane wejściowe, na przykład przez ciasteczka, pola formularzy lub zewnętrzne interfejsy.
Czy allowed_classes => false wystarcza jako ochrona?
Zapobiega tworzeniu dowolnych obiektów i zamyka tym samym najważniejszą drogę ataku. Dla danych z zewnątrz JSON jest mimo to lepszym wyborem, ponieważ parser jest prostszy i nie odtwarza obiektów.
Czy aktualizacja bibliotek pomaga przeciw atakom deserializacji?
Częściowo. Ataki często wykorzystują klasy z zależności, które przy niszczeniu lub wybudzaniu obiektu wykonują akcje. Aktualne pakiety zamykają znane łańcuchy, ale właściwą przyczyną jest wywołanie unserialize z obcymi danymi.
Czy PHP2Shell ma coś wspólnego z deserializacją?
Obie główne luki leżą w przetwarzaniu żądań w PHP. Komunikat o CVE-2026-115560 dotyczy natomiast nietypowej konfiguracji serializacji sesji. Środkiem zaradczym jest tam session.upload_progress.enabled=0.