backend
artykuł··6 min czytania

Krytyczna łatka PHP na serwerze, którego Ubuntu nie dostaje już aktualizacji

#php #php-fpm #ubuntu #docker #security #upgrades

20 października php.net zapowiedziało wydanie bezpieczeństwa na następny dzień, na 14:00 UTC. Wyszło tego dnia: PHP 8.5.12, 8.4.27, 8.3.36 i 8.2.35, z poprawkami CVE-2026-109618 i CVE-2026-109619. Obie mogą pozwolić na zdalne wykonanie kodu bez logowania; pokazany exploit dotyczył mod_php. Advisory uznaje za zagrożoną każdą instalację, która obsługuje żądania WWW: PHP-FPM, Apache z mod_php, php-cgi, FrankenPHP i LiteSpeed. O CLI nie pisze nic.

Tego wieczoru miałem do załatania trzy serwery. Domowy z Ubuntu 24.04 dostał poprawiony pakiet 8.3 od Ubuntu i paczki 8.4 i 8.5 z PPA ondreja. Drugi VPS, z Ubuntu 26.04, dostał 8.5.4-0ubuntu1.5, poza jedną aplikacją w kontenerze z własnym PHP 8.5.11, której trzeba było przebudować obraz. Trzeci, z Ubuntu 25.04, nie miał skąd jej wziąć.

Dlaczego php -v nie odpowie, czy jesteś załatany

Na Ubuntu 24.04 po instalacji poprawionego pakietu php8.3 -v dalej pokazywało 8.3.6 (gołe php to na tej maszynie 8.5 z PPA). Ubuntu przenosi poprawkę do swojego 8.3.6 i podbija tylko rewizję pakietu, więc numer z php -v się nie zmienia. Strona CVE Ubuntu podaje jako poprawione 8.3.6-0ubuntu0.24.04.13 dla 24.04 i 8.5.4-0ubuntu1.5 dla 26.04. Porównujesz więc wersję pakietu:

dpkg-query -W -f='${Package} ${Version}\n' 'php8.3-fpm'
apt-cache policy php8.3-fpm

apt-cache policy dokłada wersję zainstalowaną, kandydata i repozytoria, które te wersje teraz oferują. Pomaga to odróżnić dwa rodzaje pakietów, bo PPA ondrej/php robi odwrotnie niż Ubuntu i trzyma numery z upstreamu: po aktualizacji jego pakiety pokazują 8.4.27 albo 8.5.12. Na domowym serwerze tak właśnie było: 8.4 i 8.5 pokazywały 8.4.27 i 8.5.12, a 8.3 dalej 8.3.6, choć łatkę miało. Dlatego numer pakietu porównywałem ze stroną CVE Ubuntu.

Druga pułapka: php -v to CLI. Strony chodzą przez PHP-FPM, który może być innym pakietem, inną wersją albo kontenerem. Na drugim VPS-ie systemowe PHP miało już łatkę, a aplikacja w kontenerze dalej chodziła na 8.5.11.

Serwer, do którego nic nie przyszło

Trzeci serwer to VPS w OVH z czterema aplikacjami w Laravelu za nginx (dziewięć vhostów), na Ubuntu 25.04 z PHP-FPM 8.4.5-1ubuntu1.2. Wsparcie Ubuntu 25.04 skończyło się 15 stycznia 2026. Od tego dnia nie wchodzą do niego żadne aktualizacje pakietów, a strona CVE Ubuntu w ogóle go nie wymienia. apt nie miał nic nowszego. PPA ondreja też nie ma dystrybucji plucky: dists/plucky/Release zwraca 404, a dists/noble/Release 200.

Tego wieczoru zostały dwie drogi: ograniczyć ekspozycję i czekać albo przynieść poprawione PHP spoza systemu.

Pierwszą zrobiłem od razu. Advisory podaje środki tymczasowe i pisze wprost, że zmniejszają ekspozycję, ale instalacji bezpiecznej nie robią:

; pula FPM (php_admin_value, żeby aplikacja nie mogła tego nadpisać)
php_admin_value[memory_limit] = -1
; php.ini
post_max_size = 32M
session.upload_progress.enabled = 0

i do tego restart PHP-FPM, żeby workery wzięły nowe ustawienia (przeładowanie też by je wczytało). 32M, bo część aplikacji przyjmuje pliki; bez wysyłek advisory proponuje 8M. memory_limit = -1 też kosztuje: serwer miał 7,6 GB RAM-u i zero swapu, więc jeden request, który alokuje bez końca, mógł obudzić OOM killera, a ten nie musi wybrać akurat procesu PHP. Skończyło się na około dwunastu minutach, od 19:50 do przełączenia na kontener o 20:02 UTC. Kontener ma już poprawione PHP i zwykłe memory_limit = 128M.

Kontener na tym samym sockecie

Oficjalny obraz php:8.4.27-fpm-trixie był już w Docker Hubie. Pomysł: podmienić sam proces PHP-FPM, a nginx i aplikacje zostawić w spokoju. nginx i tak rozmawia z /run/php/php8.4-fpm.sock, więc kontener słucha dokładnie na tym sockecie.

FROM php:8.4.27-fpm-trixie
COPY --from=mlocati/php-extension-installer:2 /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath calendar exif ffi gd gettext igbinary redis intl mysqli opcache \
    ftp pcntl pdo_mysql pdo_pgsql pgsql shmop sockets sysvmsg sysvsem sysvshm xsl zip
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
COPY 90-host.ini /usr/local/etc/php/conf.d/90-host.ini
COPY zz-host.conf /usr/local/etc/php-fpm.d/zz-host.conf

Lista rozszerzeń to ta, którą miało PHP systemowe. 90-host.ini niesie ustawienia przepisane z /etc/php/8.4/fpm, w tym upload_max_filesize = 12M, poniżej post_max_size, jak każe advisory. Plik puli, bez linii pm.*:

[www]
user = www-data
group = www-data
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

Nazwa zz-host.conf jest celowa. Oficjalny obraz ustawia listen = 9000 w php-fpm.d/docker.conf, z komentarzem, że to wartość do nadpisania przez późniejsze pliki w tym katalogu. zz-host.conf sortuje się po nim, więc wygrywa jego listen.

Kontener startuje z --network host, więc aplikacje łączą się z MySQL-em, Postgresem i Redisem na 127.0.0.1 tak jak wcześniej. Całość poszła do jednego skryptu z poleceniami build, test, switch i rollback, żeby powrót był jedną komendą.

Krok test nie przełączał ruchu. Porównywał listy rozszerzeń PHP z hosta i z kontenera (comm -23 na dwóch posortowanych wynikach php -m), puszczał php-fpm -t i uruchamiał w kontenerze php artisan about dla każdej z czterech aplikacji, żeby zobaczyć, że wstają i czytają swoją konfigurację. about tylko wypisuje skonfigurowane sterowniki, więc połączenia sprawdziłem osobno z kontenera: select 1 przez fasadę DB i ping do Redisa, wszystkie cztery w porządku. Pierwsze przejście wyłapało brak ftp w kontenerze i obraz poszedł do budowy drugi raz. Dopiero potem switch:

systemctl stop php8.4-fpm
systemctl mask php8.4-fpm
mkdir -p /run/php && chown www-data:www-data /run/php
docker run -d --name php84 --restart unless-stopped \
  --network host --group-add 1000 \
  -v /var/www/html:/var/www/html -v /run/php:/run/php php84-cve:8.4.27

mask oprócz stop, żeby systemowy FPM nie wstał po restarcie i nie zabrał socketu. --group-add 1000 wzięło się z hosta, gdzie www-data należy też do grupy użytkownika od wdrożeń (1000). W kontenerze dociera tylko do mastera FPM: workery przed przejściem na www-data wołają initgroups() i biorą grupy z /etc/group w kontenerze. Sprawdziłem potem, workery puli mają tylko grupę 33. Na razie zapis działa, bo we wszystkich czterech aplikacjach storage/ należy do grupy www-data i ma prawo zapisu dla grupy (www-data to uid i gid 33 i w obrazie Debiana, i w Ubuntu); plików głębiej nie przeglądałem. rollback usuwa kontener, odmaskowuje i uruchamia FPM z systemu. Strony szły przez kontener od około 20:02 UTC, w logu błędów nginx było czysto.

Workery kolejek, cron i Reverb zostały na systemowym CLI 8.4.5. Reverb to proces CLI, który stoi za nginx i obsługuje ruch WebSocket z sieci, więc był to zakład na jedną noc, że lista zagrożonych SAPI w advisory jest pełna. Poranna aktualizacja i tak miała to PHP wymienić.

Rano: do-release-upgrade

Kontener załatał PHP stron. Reszta systemu dalej nie dostawała żadnych aktualizacji, więc następnego ranka zrobiłem aktualizację wydania. do-release-upgrade -c na 25.04 zaproponował wprost „New release '26.04.1 LTS' available”; 25.10 też jest już bez wsparcia.

Przed startem: snapshot VPS-a w panelu OVH, zrzuty MySQL-a i Postgresa, kopia /etc i crontabów oraz 2 GB swapu, którego serwer nigdy nie miał. Chciałem też zatrzymać Postgresa 17 i PHP 8.4, a posprzątać je potem ręcznie, więc dodałem /etc/update-manager/release-upgrades.d/zz-keep-obsoletes.cfg (instalator czyta tam tylko pliki *.cfg):

[Distro]
RemoveObsoletes=False

Aktualizacja szła w screen, bez pytań, z logiem do pliku:

DEBIAN_FRONTEND=noninteractive do-release-upgrade -f DistUpgradeViewNonInteractive > /root/upgrade.log 2>&1

Trwała od 8:30 do 8:46 UTC, plus restart. Trzy rzeczy z logu:

  • Po trzech minutach zniknęło sudo. Pakiety sudo i sudo-rs były rozpakowane, ale jeszcze nieskonfigurowane, a /usr/bin/sudo nie istniał. Wrócił, kiedy dpkg do nich doszedł.
  • Mniej więcej od 8:35 do 8:42 SSH odrzucało połączenia. W tym czasie stał MySQL, najpewniej dlatego, że konfigurował się jego pakiet, i jedna aplikacja odpowiadała 500. Strony główne pozostałych trzech dawały 200.
  • Postgres 17 został. Pakiety PHP 8.4 aktualizacja usunęła mimo RemoveObsoletes=False; nie sprawdzałem, który krok to zrobił. Workery kolejek w supervisorze skończyły w stanie FATAL can't find command 'php'.

Workery poszły łatwo. PHP 8.5.4 z 26.04 już było i potrzebowało rozszerzeń pgsql, redis, gd i igbinary; po ich instalacji uruchomiłem workery ponownie przez supervisorctl, a po restarcie serwera wstały same. Aplikacje mają w composer.json "php": "^8.3" (jedna ^8.2), a artisan about przeszedł na 8.5.4 bez błędów. Strony dalej idą przez kontener z 8.4.27, którego obrazu aktualizacja nie zmieniła.

Serwer ma teraz wspierane Ubuntu, ale nie jest skończony. Postgres 17 to pakiet z 25.04, którego Ubuntu już nie zaktualizuje, więc czeka go migracja na 18 albo pakiety z repozytorium samego projektu PostgreSQL. A kontener to kolejna rzecz do łatania ręcznie: przy następnej poprawce PHP trzeba podbić linię FROM i znowu przejść build, test i switch. rollback nie ma już do czego wracać, bo po aktualizacji na hoście nie ma pakietu php8.4-fpm. Czy kontener zostanie mostem, czy stałym sposobem na PHP na tym serwerze, jeszcze nie zdecydowałem.

koniec artykułu