PHP2Shell: krytyczne błędy w PHP pozwalają na zdalne wykonanie kodu. Co dzieje się wewnątrz interpretera?

Dwie krytyczne podatności w PHP pokazują, że do przejęcia serwera WWW nie zawsze potrzeba błędu w aplikacji, podatnej wtyczki czy niebezpiecznego zapytania SQL. Wystarczy nieprawidłowa obsługa pamięci w samym interpreterze. Problemy CVE-2026-109618 i CVE-2026-109619 mogą prowadzić do zdalnego wykonania kodu (RCE), a ich analiza odsłania interesujące szczegóły dotyczące działania Zend Engine, alokatora pamięci oraz cyklu życia żądań HTTP.

PHP pod ostrzałem: problem leży głębiej niż w WordPressie

21 października 2026 r. projekt PHP opublikował poprawki dwóch krytycznych podatności umożliwiających zdalne wykonanie kodu. Obie otrzymały ocenę CVSS 4.0 na poziomie 9,2 i dotyczą mechanizmów wewnętrznych interpretera, a nie konkretnej aplikacji internetowej.

Pierwsza luka, CVE-2026-109618, jest związana z obsługą sytuacji braku pamięci (Out of Memory, OOM). Druga, CVE-2026-109619, dotyczy stanu wewnętrznego, który może przetrwać zwolnienie pamięci należącej do wcześniejszego żądania.

W obu przypadkach problem nie sprowadza się do prostego przepełnienia bufora. Istotne jest to, co interpreter robi po wystąpieniu błędu: które struktury zostały już zmodyfikowane, które operacje nie zdążyły się zakończyć i czy procedury sprzątające nadal zakładają, że wszystkie dane są poprawne.

To szczególnie ważne w środowiskach, w których wiele żądań HTTP obsługuje ten sam długotrwale działający proces.

Według oficjalnego komunikatu PHP zagrożone są instalacje obsługujące żądania internetowe, w tym Apache z mod_php, PHP-FPM, PHP-CGI, FrankenPHP i LiteSpeed. Ryzyko oraz warunki wykorzystania zależą jednak od konkretnego SAPI, konfiguracji i ścieżki wykonania. https://www.php.net/archive/2026.php

CVE-2026-109618: gdy brak pamięci staje się problemem bezpieczeństwa

1. memory_limit nie jest tylko ograniczeniem wydajności

PHP pozwala określić maksymalną ilość pamięci, jaką może wykorzystać pojedyncze żądanie. Parametr memory_limit chroni serwer przed aplikacjami, które przez błąd lub nieefektywne przetwarzanie danych próbują alokować coraz więcej pamięci.

Przykładowa konfiguracja:

memory_limit = 128M
post_max_size = 8M
upload_max_filesize = 2M
max_file_uploads = 20

Wartości te są przykładami typowych ustawień, a nie uniwersalnym profilem bezpieczeństwa.

Samo osiągnięcie limitu powinno prowadzić do przerwania operacji i zgłoszenia błędu. Nie powinno natomiast pozostawiać struktur danych w stanie, który kolejne fragmenty interpretera uznają za prawidłowy.

Właśnie tutaj pojawia się problem opisany w CVE-2026-109618.

2. Przerwanie operacji w niewłaściwym momencie

PHP wykorzystuje mechanizm longjmp() do opuszczenia kontekstu wykonania w niektórych sytuacjach związanych z brakiem pamięci. Pozwala to przerwać bieżącą operację bez konieczności normalnego powrotu przez wszystkie poziomy stosu wywołań.

Mechanizm ten wymaga jednak szczególnej ostrożności. Jeżeli kod zmodyfikuje strukturę danych, a następnie spróbuje zaalokować pamięć, nie może zakładać, że dalsza część operacji zawsze zostanie wykonana.

Rozważmy uproszczony, celowo niekompletny przykład:

/* Schemat poglądowy, nie kod exploita. */

table->count++;

key = allocate_key();

if (key == NULL) {
    /* Operacja może zostać przerwana. */
    return FAILURE;
}

bucket->key = key;
bucket->value = value;

Problemem jest kolejność operacji. Licznik informujący o liczbie elementów zostaje zwiększony, zanim nowy element zostanie w pełni zainicjalizowany.

Jeśli alokacja zakończy się niepowodzeniem, kod może nie dojść do przypisania klucza i wartości. Tymczasem inna część programu może już traktować element jako istniejący.

W rzeczywistym kodzie PHP mechanizm ten jest bardziej złożony i obejmuje wewnętrzne struktury HashTable, obiekty Bucket oraz alokacje pamięci zarządzane przez Zend Engine.

Najważniejsza zasada pozostaje jednak prosta:

Nie wolno publikować stanu jako poprawnego, zanim wszystkie operacje potrzebne do jego bezpiecznego użycia nie zostaną zakończone – chyba że istnieje poprawny mechanizm wycofania zmian.

3. Dlaczego sprzątanie pamięci może pogłębić błąd?

Po przerwaniu operacji PHP musi zakończyć obsługę żądania. Procedury sprzątające zwalniają zasoby, niszczą wartości zmiennych i porządkują struktury danych.

Jeżeli struktura zawiera niekompletny element, procedura sprzątająca może odczytać nieprawidłowe dane. W zależności od stanu pamięci może to doprowadzić do błędnego odczytu wskaźnika, nieprawidłowego zwolnienia zasobu lub dalszego uszkodzenia pamięci.

W przypadku omawianej podatności oficjalny opis wskazuje na sytuację, w której PHP po nieudanej alokacji pozostawia wewnętrzny stan nieprawidłowy, a późniejsze procedury kończenia żądania traktują go jak poprawny. https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w

W sprzyjających warunkach problem może prowadzić do zdalnego wykonania kodu z uprawnieniami procesu obsługującego żądanie.

Jak żądanie HTTP może doprowadzić do OOM?

Ciekawym elementem analizy jest sposób, w jaki pozornie niewielkie żądanie może spowodować znaczące zużycie pamięci.

PHP obsługuje dane formularzy automatycznie, jeszcze zanim aplikacja zacznie wykonywać własny kod. Dotyczy to między innymi formularzy przesyłanych w formacie multipart/form-data, używanym powszechnie przy wysyłaniu plików.

Treść przesyłana przez sieć nie musi odpowiadać ilości pamięci potrzebnej do jej reprezentacji wewnątrz interpretera.

Na zużycie pamięci wpływają m.in.:

  • liczba pól formularza;
  • liczba części multipart;
  • liczba nagłówków przypadających na część;
  • zagnieżdżenie nazw pól;
  • metadane tworzonych elementów $_POST i $_FILES;
  • narzut struktur haszujących i alokacji natywnych.

Przykładowo zagnieżdżona nazwa pola może wymagać utworzenia wielu tablic PHP. Każda tablica i każdy wpis wiążą się z dodatkowym narzutem pamięci. W rezultacie niewielki rozmiar żądania na poziomie HTTP nie gwarantuje niewielkiego zużycia pamięci przez PHP.

Istotna jest również różnica między post_max_size a memory_limit. Pierwszy parametr ogranicza rozmiar danych POST w określonych warunkach, a drugi – zużycie pamięci przez żądanie. Nie są to równoważne zabezpieczenia.

Oficjalny opis CVE-2026-109618 wskazuje m.in. na problemy z żądaniami bez użytecznej wartości Content-Length, narzutem nagłówków multipart oraz metadanymi pustych pól przesyłania plików. https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w

CVE-2026-109619: use-after-free i nieaktualne wskaźniki

Druga podatność dotyczy klasy błędów znanej jako use-after-free.

Występuje ona wtedy, gdy program nadal korzysta z obiektu lub obszaru pamięci po jego zwolnieniu. Samo zwolnienie pamięci nie zeruje automatycznie wszystkich wskaźników, które mogły do niej prowadzić.

W przypadku PHP problem dotyczy stanu wewnętrznego utrzymywanego przez proces obsługujący żądania. Niektóre zmienne globalne modułów mogą przechowywać wskaźniki do danych należących do konkretnego żądania.

W normalnych warunkach dane te powinny zostać zresetowane przed zwolnieniem pamięci żądania albo zanim kolejna operacja zacznie z nich korzystać.

Jeśli operacja zostanie przerwana przez błąd krytyczny, odpowiedni etap czyszczenia może zostać pominięty.

Uproszczony schemat wygląda następująco:

  1. Żądanie A tworzy obiekt w pamięci należącej do żądania.
  2. Wskaźnik do obiektu zostaje zapisany w stanie współdzielonym przez kolejne operacje procesu.
  3. Błąd przerywa operację przed wyzerowaniem wskaźnika.
  4. PHP zwalnia pamięć należącą do żądania A.
  5. Ten sam proces obsługuje żądanie B.
  6. Kod wykonywany przed pełną inicjalizacją nowego żądania odczytuje nieaktualny wskaźnik.

Jeżeli zwolniony obszar pamięci został ponownie wykorzystany, odczyt może dotyczyć zupełnie innych danych. W określonych warunkach możliwe staje się wykorzystanie tej sytuacji do naruszenia integralności pamięci interpretera.

Dlaczego samo zwolnienie pamięci nie wystarcza?

W środowiskach wielożądaniowych ważne jest rozróżnienie między pamięcią należącą do pojedynczego żądania a stanem procesu, który może przetrwać dłużej.

W PHP-FPM worker obsługuje kolejne żądania. Nie oznacza to, że wszystkie jego globalne struktury są automatycznie niszczone i tworzone od początku przy każdym połączeniu. Część stanu jest zarządzana przez cykl życia modułów i wymaga prawidłowego wykonania procedur inicjalizacyjnych oraz kończących.

Dlatego szczególnie niebezpieczne są sytuacje, w których:

  • dane należące do żądania trafiają do długotrwale przechowywanej struktury;
  • występuje błąd przerywający normalny przebieg operacji;
  • sprzątanie nie usuwa wszystkich odwołań do tych danych;
  • kolejna operacja korzysta z nieaktualnego stanu przed jego resetem.

Szczegóły tej klasy problemów opisano w oficjalnym advisory CVE-2026-109619.

Zend Engine: dlaczego kolejność operacji ma znaczenie?

PHP jest językiem wysokiego poziomu, ale jego interpreter został napisany przede wszystkim w C. Oznacza to, że część podstawowych mechanizmów, w tym zarządzanie pamięcią, struktury danych i obsługa błędów, opiera się na kodzie natywnym.

Zend Engine wykorzystuje m.in. strukturę zval do reprezentowania wartości PHP oraz HashTable do implementacji tablic i innych struktur opartych na haszowaniu.

W uproszczeniu zval przechowuje wartość i informację o jej typie, natomiast tablica haszująca zarządza zestawem elementów, kluczami i wartościami.

Poprawność tych struktur zależy od zachowania określonych niezmienników. Jeśli licznik elementów wskazuje więcej wpisów, niż faktycznie zainicjalizowano, albo wskaźnik prowadzi do zwolnionego obszaru pamięci, kod korzystający z tych struktur może działać na nieprawidłowych danych.

W aplikacji PHP większość programistów nie musi zarządzać pamięcią ręcznie. W kodzie C odpowiedzialnym za implementację interpretera sytuacja wygląda inaczej: nawet pojedyncza błędna kolejność operacji może mieć konsekwencje dla całego procesu.

Z punktu widzenia audytu bezpieczeństwa warto szukać następujących wzorców:

  • zwiększania liczników przed zakończeniem inicjalizacji elementu;
  • publikowania wskaźnika przed przygotowaniem wskazywanej struktury;
  • operacji mogących zakończyć się błędem pomiędzy zmianą stanu a jego zatwierdzeniem;
  • ścieżek błędów pomijających sprzątanie lub wycofanie zmian;
  • globalnego stanu przechowującego wskaźniki do danych o krótszym czasie życia;
  • procedur destrukcji zakładających, że struktury są zawsze w pełni zainicjalizowane.

To przydatna metoda analizy nie tylko PHP, lecz także innych interpreterów, bibliotek natywnych i serwerów napisanych w C lub C++.

Apache mod_php a PHP-FPM: różnica tkwi w cyklu życia procesu

Jednym z najciekawszych elementów omawianych podatności jest to, że różne sposoby uruchamiania PHP prowadzą do odmiennych ścieżek obsługi błędów.

Apache z mod_php

W przypadku mod_php interpreter działa jako moduł Apache. Zgodnie z analizą opublikowaną przez projekt PHP, gdy inicjalizacja żądania zakończy się niepowodzeniem, Apache może mimo tego przejść do procedury zamykania żądania.

Jeżeli w tej sytuacji struktury wewnętrzne są nieprawidłowe, procedury sprzątające mogą wykorzystać niespójny stan pamięci. Dla CVE-2026-109618 potwierdzono możliwość zdalnego wykonania kodu przed rozpoczęciem wykonywania kodu konkretnej aplikacji PHP. https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w

PHP-FPM

PHP-FPM wykorzystuje osobne procesy robocze, które obsługują żądania przekazywane przez serwer WWW, na przykład Nginx lub Apache z mod_proxy_fcgi.

W przypadku niepowodzenia podczas wczesnej inicjalizacji żądania PHP-FPM może zakończyć proces roboczy zamiast kontynuować jego obsługę. Ogranicza to możliwość wykorzystania niektórych stanów pozostawionych po błędzie.

Nie oznacza to jednak, że PHP-FPM automatycznie eliminuje wszystkie problemy z pamięcią. Błąd występujący już po poprawnej inicjalizacji żądania może prowadzić do innej ścieżki zamykania, a proces może pozostać aktywny i obsłużyć następne żądanie.

Właśnie taki rodzaj różnicy ma znaczenie dla CVE-2026-109619. Oficjalny opis PHP wskazuje na możliwość utrzymywania nieaktualnego stanu pomiędzy żądaniami, zależnie od konkretnej operacji, konfiguracji i sposobu obsługi błędu.

Wniosek dla administratorów: przejście z mod_php na PHP-FPM może zmienić warunki wykorzystania części podatności, ale nie zastępuje aktualizacji interpretera.

Co to oznacza dla WordPressa?

WordPress jest napisany w PHP, więc podatność interpretera może wpływać na serwis niezależnie od tego, czy sam WordPress zawiera błąd w swoim kodzie.

W kontekście analizy warto zwrócić uwagę na funkcje deserializacji danych. WordPress wykorzystuje m.in. maybe_unserialize() do obsługi wartości, które mogą być zapisane w formacie serializowanym.

Przykładowy fragment kodu aplikacji:

<?php

$value = get_option('example_setting');
$data = maybe_unserialize($value);

Sam taki kod nie oznacza podatności. Funkcja może być wykorzystywana prawidłowo, a dane mogą pochodzić z zaufanego źródła.

Znaczenie ma jednak to, że WordPress i jego wtyczki mogą docierać do wewnętrznych mechanizmów deserializacji PHP. W określonych scenariuszach aplikacyjnych operacje tego typu mogą być częścią ścieżki prowadzącej do błędu w interpreterze.

Dlatego aktualizacja samych plików WordPressa nie wystarczy, jeśli serwer nadal korzysta z podatnej wersji PHP.

Należy sprawdzić zarówno wersję CMS-a, jak i wersję interpretera używaną przez rzeczywisty proces obsługujący stronę. Warto pamiętać, że wersja PHP w terminalu może różnić się od wersji skonfigurowanej dla PHP-FPM lub Apache.

Jak sprawdzić wersję PHP w Linuksie?

Podstawową informację można uzyskać poleceniem:

php -v

Polecenie pokazuje wersję interpretera dostępnego w CLI. Nie gwarantuje jednak, że identyczną wersję wykorzystuje serwer WWW.

W systemach Debian i Ubuntu można sprawdzić zainstalowane pakiety:

dpkg -l 'php*' | grep '^ii'

W systemach opartych na RPM:

rpm -qa | grep '^php'

Jeśli aplikacja korzysta z PHP-FPM, warto również sprawdzić status usługi, np.:

systemctl status php8.4-fpm

Nazwa usługi zależy od dystrybucji i zainstalowanej wersji PHP.

W środowiskach korzystających z wielu wersji interpretera trzeba zweryfikować konfigurację konkretnej witryny, w tym pulę PHP-FPM oraz konfigurację serwera WWW. Nie należy polegać wyłącznie na wyniku php -v.

Jak zabezpieczyć serwer? Najpierw aktualizacja

Poprawione wersje opublikowane przez projekt PHP to:

Gałąź PHPWersja zawierająca poprawki
PHP 8.58.5.12
PHP 8.48.4.27
PHP 8.38.3.36
PHP 8.28.2.35

Źródło: oficjalne archiwum PHP.

W pierwszej kolejności należy sprawdzić, czy dystrybucja Linuksa udostępniła aktualizację pakietów. Dostawcy systemów z długoterminowym wsparciem mogą przygotowywać poprawki dla własnych pakietów, również dla starszych wersji interpretera.

Na Debianie lub Ubuntu aktualizację pakietów można rozpocząć od:

sudo apt update
sudo apt upgrade

Na systemach opartych na RPM, korzystających z DNF:

sudo dnf upgrade

Są to ogólne polecenia aktualizacji systemu, a nie gwarancja, że poprawiony PHP znajduje się już w skonfigurowanych repozytoriach. Po aktualizacji trzeba sprawdzić wersję pakietu i zrestartować odpowiednie usługi, jeśli jest to wymagane.

W przypadku instalacji kompilowanej samodzielnie należy pobrać odpowiednie poprawione źródła i zbudować interpreter ponownie. PHP 8.1 i starsze gałęzie nie otrzymują już standardowych aktualizacji od projektu PHP, choć niektórzy dostawcy mogą udostępniać własne poprawki. Jeżeli to możliwe, należy zaplanować migrację do wspieranej wersji.

Tymczasowe zabezpieczenia: co można zrobić przed aktualizacją?

Projekt PHP opublikował również środki ograniczające ryzyko. Nie zastępują one poprawki, ale mogą pomóc ograniczyć wybrane ścieżki ataku.

1. Ogranicz rozmiar i złożoność żądań

Warto przeanalizować parametry:

post_max_size = 8M
upload_max_filesize = 2M
max_input_vars = 1000
max_input_nesting_level = 64

To przykładowe wartości, które należy dostosować do aplikacji. Jeżeli serwis obsługuje duże pliki, ograniczenia mogą wymagać innych ustawień.

Ochrona powinna działać również na poziomie serwera WWW lub reverse proxy. Należy ograniczać nie tylko rozmiar przesyłanych danych, ale także liczbę części multipart, nagłówków, pól oraz głębokość zagnieżdżenia. Ważne jest, aby kontrola obejmowała rzeczywistą treść żądania, a nie wyłącznie deklarowany nagłówek Content-Length.

2. Rozważ wyłączenie śledzenia postępu wysyłania plików

Dla ścieżek związanych z CVE-2026-109619 oficjalne zalecenie obejmuje ustawienie:

session.upload_progress.enabled = 0

Zmiana powinna zostać wprowadzona w konfiguracji stosowanej przed rozpoczęciem parsowania żądania, a następnie należy zrestartować procesy PHP. Samo wywołanie ini_set() w kodzie aplikacji nie jest równoważnym zabezpieczeniem, ponieważ może zostać wykonane zbyt późno.

Wyłączenie tej funkcji ogranicza potwierdzone ścieżki wykorzystania, ale nie stanowi pełnego rozwiązania problemu.

3. Ostrożnie z memory_limit = -1

Oficjalny opis CVE-2026-109618 wymienia również administracyjne ustawienie memory_limit = -1 jako tymczasowe ograniczenie konkretnej ścieżki wykorzystania kontrolowanego błędu OOM.

Nie jest to jednak uniwersalna rekomendacja dla każdego serwera. Usunięcie limitu pamięci PHP może pozwolić błędnemu lub złośliwemu żądaniu zużyć znacznie więcej zasobów i doprowadzić do niedostępności usługi. Co więcej, nie eliminuje wszystkich możliwych błędów alokacji.

Taką zmianę należy rozważać wyłącznie po analizie oficjalnych zaleceń, architektury serwera oraz dostępnych limitów systemowych. Nie należy zastępować nią aktualizacji.

Pełne wskazówki znajdują się w oficjalnym komunikacie PHP.

Jak projektować bezpieczniejszy kod w PHP?

Choć omawiane błędy znajdują się w interpreterze, stanowią dobrą lekcję dla osób rozwijających aplikacje i rozszerzenia PHP w C.

W kodzie aplikacji warto przede wszystkim:

  • ograniczać rozmiar danych przyjmowanych od użytkownika;
  • walidować dane przed ich kosztownym przetwarzaniem;
  • unikać deserializacji niezaufanych danych, jeśli nie jest konieczna;
  • stosować aktualne wersje bibliotek i rozszerzeń;
  • obsługiwać błędy tak, aby nie pozostawiały aplikacji w niespójnym stanie;
  • nie zakładać, że catch (Throwable $e) przechwyci każdy błąd interpretera lub problem z pamięcią.

Ostatni punkt jest szczególnie ważny. Błąd krytyczny związany z alokacją pamięci nie musi zachowywać się jak zwykły wyjątek aplikacyjny. W przypadku przerwania wykonywania kodu natywnego nie można zakładać, że aplikacja będzie miała możliwość przeprowadzenia pełnego sprzątania.

Dla osób rozwijających rozszerzenia w C ważna jest natomiast zasada bezpiecznego zarządzania zasobami: struktura powinna być albo w pełni zainicjalizowana, albo wyraźnie oznaczona jako niekompletna, a ścieżki błędów muszą potrafić bezpiecznie obsłużyć oba przypadki.

Podsumowanie: bezpieczeństwo zaczyna się poniżej warstwy aplikacji

Podatności CVE-2026-109618 i CVE-2026-109619 pokazują, że bezpieczeństwo serwisu internetowego zależy nie tylko od kodu WordPressa, frameworka czy wtyczek. Równie ważny jest interpreter, który przetwarza każde żądanie i zarządza pamięcią używaną przez aplikację.

W pierwszym przypadku problemem jest niespójny stan po przerwaniu alokacji pamięci. W drugim – możliwość wykorzystania nieaktualnego stanu wewnętrznego, który przetrwał zwolnienie pamięci należącej do wcześniejszego żądania.

Dla administratorów Linuksa oznacza to konieczność sprawdzenia wersji PHP, aktualizacji pakietów oraz weryfikacji konfiguracji wszystkich usług korzystających z interpretera. Dla programistów jest to przypomnienie, że obsługa błędów i cykl życia pamięci są elementami bezpieczeństwa, a nie tylko szczegółami implementacyjnymi.

Najważniejsze działanie jest jedno: zaktualizować PHP do wersji zawierającej poprawki. Konfiguracja serwera, ograniczenia żądań i wyłączenie wybranych funkcji mogą zmniejszyć ryzyko, ale nie zastępują naprawy podatnego interpretera.

About the author

Autor "BIELI" to zapalony entuzjasta otwartego oprogramowania, który dzieli się swoją pasją na blogu poznajlinuxa.pl. Jego wpisy są skarbnicą wiedzy na temat Linuxa, programowania oraz najnowszych trendów w świecie technologii. Autor "BIELI" wierzy w siłę społeczności Open Source i zawsze stara się inspirować swoich czytelników do eksplorowania i eksperymentowania z kodem.