Krótko: Każda strona na serwerze powinna mieć własną pulę PHP-FPM z własnym użytkownikiem systemowym i własnym gniazdem. Rozdzielenie działa jednak dopiero ze szczegółami: uprawnienia do gniazda tylko dla serwera WWW, security.limit_extensions ustawione na .php, brak publicznie dostępnych stron statusu, własne limity procesów i czasu działania oraz oddzielne logi. Przed samym PHP2Shell chroni wyłącznie aktualizacja; pule ograniczają zasięg ataku.
Jeden serwer, wiele stron, wspólne ryzyko
Na wielu serwerach przez lata narosło to, czego nikt tak nie planował. Strona firmowa, sklep, strona kampanii z 2019 roku i system testowy działają obok siebie, często pod tym samym użytkownikiem i w tej samej puli. Jeśli zapomniana strona kampanii zostanie przejęta, otwarte są też dane klientów sklepu.
PHP-FPM, menedżer procesów FastCGI dla PHP, rozwiązuje to za pomocą pul: grup procesów roboczych z własnymi ustawieniami. Jak działa ta podstawowa zasada, wyjaśnia artykuł PHP-FPM wyjaśniony: menedżer procesów, pule i procesy robocze. Budowę oddzielnych pul z własnym użytkownikiem, własnym katalogiem tymczasowym i open_basedir opisuje poradnik open_basedir i izolacja dla każdej strony. Ten artykuł idzie o krok dalej: jak sprawdzić istniejące pule i jakie ustawienia czynią rozdzielenie wiarygodnym?
Ważne dla oceny: w PHP2Shell luki CVE-2026-109618 i CVE-2026-109619 leżą w interpreterze PHP, który współdzielą wszystkie pule danej wersji PHP. Osobna pula nie zapobiega wykorzystaniu luki. Rozstrzyga jednak, czy atakujący dotrze potem do jednej strony, czy do wszystkich.
Pule PHP-FPM z osobnym użytkownikiem: sprawdzenie stanu
Zanim cokolwiek Państwo zmienią, proszę zyskać przegląd. Poniższe polecenia tylko odczytują. Ścieżki i numery wersji różnią się zależnie od dystrybucji:
# Wszystkie pule z użytkownikiem i gniazdem
grep -HE '^\s*(\[|user|group|listen)\s*' /etc/php/*/fpm/pool.d/*.conf
# Działające pule i ich użytkownicy
ps -eo user,pid,cmd | grep '[p]hp-fpm: pool'
# Gniazda i ich uprawnienia
ls -l /run/php/
# Sprawdzić konfigurację pod kątem błędów (niczego nie zmienia)
sudo php-fpm8.3 -t
Proszę zwrócić uwagę na trzy wzorce: kilka stron w tej samej puli, pule działające jako www-data lub nawet jako root oraz gniazda zapisywalne dla wszystkich użytkowników. Każdy z nich podważa rozdzielenie.
Jeśli na serwerze działa kilka wersji PHP, każda z nich ma własną usługę FPM z własnymi katalogami pul. Proszę sprawdzić wszystkie, nie tylko wersję skonfigurowaną jako ostatnia. Szczególnie stare wersje z zapomnianymi pulami są ryzykiem, ponieważ często nie są ani konserwowane, ani restartowane po aktualizacjach. Wskazówki znajdują się w artykule Wiele wersji PHP na jednym serwerze. Proszę zapisać wynik w małej tabeli: strona, pula, użytkownik, gniazdo, wersja PHP. Ten przegląd jest zarazem podstawą każdej późniejszej zmiany.
Najważniejsze ustawienia dla każdej puli
Poniższy przegląd opiera się na konfiguracji FPM z podręcznika PHP. Wartości dla procesów i czasów zależą od obciążenia i aplikacji:
| Ustawienie | Cel | Zalecenie |
|---|---|---|
user, group |
użytkownik procesów roboczych | osobny użytkownik dla każdej strony, nigdy root |
listen.owner, listen.group, listen.mode |
uprawnienia do gniazda | tylko grupa serwera WWW, tryb 0660 |
listen.acl_users |
dostęp przez ACL zamiast właściciela | wpisać tylko użytkownika serwera WWW |
security.limit_extensions |
wykonywalne rozszerzenia | ograniczyć do .php (domyślnie .php .phar) |
clear_env |
czyszczenie zmiennych środowiskowych | pozostawić domyślne yes |
pm.max_children |
maksymalna liczba procesów roboczych | ograniczyć tak, aby jedna strona nie obciążyła całego serwera |
request_terminate_timeout |
przerywanie zawieszonych żądań | ustawić stosownie do najdłuższego uzasadnionego żądania |
pm.status_path, ping.path |
strona statusu i ping | ustawiać tylko w razie potrzeby i zabezpieczyć wewnętrznie |
slowlog, request_slowlog_timeout |
dziennik wolnych żądań | dla każdej puli w osobnym pliku |
Fragment utwardzonej puli wygląda tak:
; /etc/php/8.3/fpm/pool.d/shop.conf (fragment)
[shop]
user = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
request_terminate_timeout = 60s
security.limit_extensions = .php
clear_env = yes
slowlog = /var/log/php/shop.slow.log
request_slowlog_timeout = 10s
php_admin_value[error_log] = /var/log/php/shop.error.log
Ustawienia takie jak memory_limit czy session.upload_progress.enabled ustawiają Państwo dla każdej puli za pomocą php_admin_value i php_admin_flag. Według podręcznika tych wartości nie można nadpisać przez ini_set(). Więcej w artykule php_admin_value: blokowanie ustawień przed nadpisaniem.
Strony statusu i wymiana procesów roboczych: właściwa ocena
pm.status_path i ping.path są aktywne tylko wtedy, gdy zostały ustawione. Strona statusu pokazuje działające żądania, procesy i obciążenie. Dla monitoringu jest to pomocne, ale publicznie dostępna stanowi zbędne źródło informacji dla atakujących. Proszę przekazywać te ścieżki w serwerze WWW wyłącznie dla adresów wewnętrznych. Jak bezpiecznie współpracują Nginx i PHP-FPM, opisuje artykuł Zabezpieczanie Nginx z PHP-FPM.
pm.max_requests określa, po ilu żądaniach proces roboczy uruchamia się od nowa; wartość domyślna to 0, czyli nigdy. Wartość większa od zera pomaga przy powoli rosnącym zużyciu pamięci. Jako ochrona przed PHP2Shell nie jest przeznaczona: php.net nie wymienia jej wśród środków tymczasowych i podkreśla, że w pełni chroni tylko aktualizacja. Po aktualizacji obowiązuje ponadto: wszystkie procesy robocze wszystkich pul muszą zostać zrestartowane, ponieważ w przeciwnym razie stare procesy działają dalej z podatnym kodem.
Proszę utrzymywać szablon dla nowych pul i zakładać według niego każdą nową stronę. Wtedy rozdzielenie powstaje automatycznie, zamiast być dorabiane po latach.
Typowe błędy przy przestawianiu
Najczęstszym błędem jest nowa pula, która działa wprawdzie pod własnym użytkownikiem, ale której pliki nadal należą do www-data. Wtedy pula nie może zapisywać tam, gdzie powinna, a ktoś nadaje uprawnienia zbyt hojnie. Proszę wcześniej ustalić, które katalogi muszą być zapisywalne. Drugim błędem są zaplanowane zadania, które nadal działają pod starym użytkownikiem i tworzą pliki z niewłaściwym właścicielem. Trzecim jest wspólny katalog sesji dla wszystkich pul, który znosi rozdzielenie.
Po każdym przestawieniu proszę przetestować logowanie, uploady, pamięci podręczne i zaplanowane zadania strony oraz obserwować log błędów puli.
Jak konkretnie pomaga PHP SOS?
- Inwentaryzujemy wszystkie pule, użytkowników, gniazda i ustawienia oraz pokazujemy, gdzie strony dziś współdzielą procesy lub uprawnienia.
- Konfigurujemy dla każdej strony utwardzoną pulę, z uprawnieniami do gniazda, ograniczeniem rozszerzeń, limitami zasobów i własnymi logami.
- Zabezpieczamy strony statusu, kontrolowanie restartujemy wszystkie procesy robocze i testujemy każdą stronę.
- W ramach utwardzania serwerów dokumentujemy rozdzielenie jako wzorzec dla przyszłych projektów.
Częste pytania
Dlaczego każda strona powinna mieć własną pulę PHP-FPM?
Osobna pula z własnym użytkownikiem systemowym rozdziela strony na poziomie procesów i uprawnień do plików. Jeśli jedna strona zostanie przejęta, kod nie może bez przeszkód czytać plików pozostałych. Ponadto limity i ustawienia można określać dla każdej strony oddzielnie.
Czy osobna pula chroni przed PHP2Shell?
Nie. Luki leżą w samym interpreterze PHP, którego wszystkie pule używają wspólnie. Ochronę zapewnia tylko aktualizacja do naprawionej wersji z restartem wszystkich procesów roboczych. Oddzielne pule ograniczają jednak zasięg udanego ataku.
Co daje security.limit_extensions?
Według podręcznika PHP to ustawienie ogranicza, jakie rozszerzenia plików PHP-FPM wykonuje jako skrypt główny. Domyślnie są to .php i .phar. Kto ograniczy je do .php, zapobiega wykonaniu na przykład przesłanych plików graficznych jako PHP, jeśli serwer WWW jest błędnie skonfigurowany.
Czy strona statusu PHP-FPM jest niebezpieczna?
Zdradza szczegóły o działających procesach i żądaniach. Jeśli jest publicznie dostępna bez zabezpieczenia, ułatwia atakującym orientację. Według podręcznika jest aktywna tylko wtedy, gdy ustawiono pm.status_path. Wtedy serwer WWW powinien ograniczyć tę ścieżkę do adresów wewnętrznych.