CVE-2026-109618 and CVE-2026-109619: What you should do now
Details of two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, leaked on 20 October 2026, before PHP had released a fix. PHP's security…
PHP fixed the two critical vulnerabilities CVE-2026-109618 and CVE-2026-109619 on October 21, 2026, in PHP 8.3.36 and the matching 8.2.35, 8.4.27 and 8.5.12 releases. Both allow remote code execution, which means an attacker can run their own code on the server through requests to your site. If your server runs Ubuntu 20.04, apt upgrade doesn't bring you any of these versions. The PHP 8 packages on Ubuntu come from Ondřej Surý's PPA, a third-party package repository on Launchpad, and it no longer builds anything for 20.04.
Two production Laravel servers I look after for a client were in exactly that state. I didn't want to run an Ubuntu release upgrade on a live server in the middle of a security incident, so I ran PHP 8.3.36 from Docker's official image next to the existing setup and pointed nginx at it. I covered the vulnerabilities themselves in my earlier post. This one is only about getting the fixed version onto an old server.
Ubuntu's own 20.04 archive only ships PHP 7.4, so anyone running PHP 8 on 20.04 got it from ppa:ondrej/php. On October 21 the PPA published 8.3.36 for Ubuntu 24.04 and 22.04. For 20.04 it has nothing. The last PHP 8.3 build for 20.04 was 8.3.21 in May 2025, and Launchpad lists it as deleted. Ondřej's other repository, packages.sury.org, has no 20.04 section at all.
Ubuntu ended standard security maintenance for 20.04 in May 2025, the same month as that last build. Ubuntu Pro keeps patching Ubuntu's own 20.04 packages until May 2030, but a PPA is a separate repository, so Pro doesn't cover PHP 8. Ubuntu 18.04 has no fixed builds either.
So a PHP 8 install on 20.04 stopped getting security fixes in May 2025 at the latest. The two servers I had to fix were still on 8.3.12 and 8.3.10.
On a typical nginx server, nginx doesn't run PHP itself. It passes every PHP request on to PHP-FPM, a separate process manager that keeps a pool of PHP workers running. The two talk over FastCGI, the protocol nginx uses to hand requests to PHP-FPM, and one line in the nginx site config, fastcgi_pass, says where PHP-FPM listens. Usually that's a Unix socket like /var/run/php/php8.3-fpm.sock.
That makes PHP-FPM easy to replace without touching anything else. Docker's official php image had tags for all four fixed versions on Docker Hub by the evening of October 21 (UTC): 8.3.36-fpm-bookworm, 8.4.27-fpm-bookworm, 8.2.35-fpm-bookworm and 8.5.12-fpm-trixie. The container brings its own Debian libraries, so the age of the host's Ubuntu doesn't matter. The host keeps nginx, the TLS certificates, the database, Redis and the site files. The container only runs PHP-FPM. It reads the site through a bind mount, which makes a host directory visible inside the container, and listens on a local port that nginx can reach. On 20.04, Docker itself comes from Ubuntu's own archive with apt install docker.io.
The official image comes with a basic set of PHP extensions. Run php8.3 -m on the host to see what your site uses. For the missing ones I used mlocati's install-php-extensions, version 2.12.3, which installs an extension together with the system libraries it needs.
FROM php:8.3.36-fpm-bookworm
ADD https://github.com/mlocati/docker-php-extension-installer/releases/download/2.12.3/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions \
&& install-php-extensions bcmath intl opcache pdo_mysql redis \
&& groupadd -g 1000 forge \
&& useradd -u 1000 -g 1000 -G www-data -M -d /home/forge -s /usr/sbin/nologin forge \
&& rm -f /usr/local/etc/php-fpm.d/zz-docker.conf
COPY pool.conf /usr/local/etc/php-fpm.d/www.conf
Replace the extension list with yours. The user inside the container needs the same numeric user and group IDs as the owner of the site on the host, because it reads and writes the site's files through the bind mount. On a Laravel Forge server that's forge with ID 1000. The -G www-data part is what I missed the first time. Laravel's storage directories on those servers belong to the group www-data with mode 775, so without that group membership every page failed with a 500 error and "Permission denied" in the log. www-data has group ID 33 on both Debian and Ubuntu, so the membership carries over to the host's files.
The image also ships a zz-docker.conf that makes PHP-FPM listen on port 9000 on all interfaces, as the official Dockerfile shows. I delete it and set up the pool myself in pool.conf:
[global]
error_log = /proc/self/fd/2
daemonize = no
[www]
user = forge
group = forge
listen = 127.0.0.1:9836
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
The deleted file also contained daemonize = no, which keeps PHP-FPM in the foreground the way Docker needs it, so the pool file sets it again. Port 9836 is arbitrary, so pick any free one. Copy the pm values from the host pool in /etc/php/8.3/fpm/pool.d/www.conf so the container runs the same number of workers.
docker build -t php-fpm-fixed:8.3.36 .
docker run -d --name php-fixed --restart unless-stopped --network host \
-v /home/forge:/home/forge \
-v /etc/php/8.3/fpm/php.ini:/usr/local/etc/php/php.ini:ro \
php-fpm-fixed:8.3.36
With --network host the container listens on the host's own 127.0.0.1, where nginx can reach it and the internet can't. The site is mounted at the same path it has on the host. That matters because nginx tells PHP-FPM which file to run by its full path. Mounting the host's php.ini keeps your memory limit, upload sizes and other settings, and --restart unless-stopped brings the container back after a reboot.
If your app uses PHP's native file sessions instead of keeping sessions itself the way Laravel does, mount the session directory too. Ubuntu's PHP stores them in /var/lib/php/sessions, while the official image would use /tmp inside the container, so set session.save_path to the mounted directory or your users get logged out when you switch.
Before changing nginx, check the container. These commands show the version, list the modules the host has and the container doesn't, and send one real request to it. cgi-fcgi is in the libfcgi-bin package.
docker exec php-fixed php -v
comm -23 <(php8.3 -m | sort -u) <(docker exec php-fixed php -m | sort -u)
SCRIPT_FILENAME=/home/forge/example.com/public/index.php \
REQUEST_METHOD=GET REQUEST_URI=/ HTTP_HOST=example.com \
cgi-fcgi -bind -connect 127.0.0.1:9836 | head -20
On my servers the module comparison still listed ftp and swoole, which the web app didn't use.
In the site's nginx config, change fastcgi_pass in the PHP location:
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9836;
# the rest of the block stays as it was
}
Then run nginx -t && systemctl reload nginx. The image logs every request to standard error, so docker logs --since 1m php-fixed should now show your traffic. Going back is the same edit with the old socket path.
If the server is managed by Laravel Forge, make the change in Forge's nginx editor for the site, not in the file on disk, because Forge owns that file. Switching the site's PHP version in Forge can also rewrite fastcgi_pass back to the old socket.
The container cost me about 5 to 10 ms per page. With direct FastCGI requests, three pages took 109, 102 and 98 ms on the host's PHP-FPM socket and 113, 111 and 111 ms through the container.
Only web requests run the new PHP. The command-line PHP on the host is still the old version, and that includes queue workers, the cron scheduler and everything you start with php artisan. Both advisories describe attacks over the network, through requests that PHP processes. Queue workers and cron jobs don't parse incoming web requests, so I treated them as much less exposed, but they still run a PHP version that stopped getting security fixes in May 2025 at the latest. They can run in the same container with docker exec -u forge php-fixed php artisan ... if you want them covered too.
The operating system is also still 20.04, without standard security updates since May 2025. The container is a bridge, and I upgraded Ubuntu on both servers the next day.
I upgraded in place with do-release-upgrade, from 20.04 to 22.04 and then to 24.04, after taking a snapshot of each server. Then I installed 8.3.36 from the PPA on the host, switched fastcgi_pass back to the socket and stopped the container. The live site was unreachable only during its two reboots, about a minute each. These are the problems I ran into:
mirror.hetzner.com. The upgrader treats it as an unknown mirror, disables all of those entries and then fails because it can't find ubuntu-minimal, leaving a half-rewritten sources.list behind. The original is saved as sources.list.distUpgrade. Switch the entries to archive.ubuntu.com and security.ubuntu.com before you start.ppa:ondrej/php.Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com