On October 21 the Italian CERT-AGID flagged two critical vulnerabilities in the PHP core, CVE-2026-109618 and CVE-2026-109619, both leading to remote code execution. Fixed releases came out the same day. I checked my services: one was already up to date, one does not use PHP, one ran PHP-FPM in a Docker container whose image had not been rebuilt in weeks. The third one is the story worth telling.
What the official sources say
I took the facts from the CERT-AGID bulletin (in Italian) and checked them against the PHP project's GitHub advisories (GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp), the php.net announcements and NVD.
- What they are. Memory management bugs in the engine, mainly classified as use-after-free. The first concerns how PHP tears down a request after running out of memory; the second, internal state that is not reset between one request and the next on the same process.
- Severity. PHP scores them 9.2 in CVSS v4.0. NVD lists 9.8 in CVSS 3.1. Different numbers on different scales, same conclusion: critical.
- Who is exposed. With mod_php (PHP loaded inside Apache) code execution is verified. With PHP-FPM the advisories describe partial or conditional exposure, demonstrated only in application-dependent scenarios. "Conditional" does not mean "safe".
- Fixed versions. 8.5.12, 8.4.27, 8.3.36 and 8.2.35, all released on October 21.
- Old branches. 8.1 and earlier get no more releases. The security team published minimal source-only fixes for 7.4, 8.0 and 8.1, useful if you build PHP yourself. For everyone else the answer is moving to a supported branch. And 8.2 only gets security fixes until December 31, 2026.
The CERT-AGID bulletin also mentions proofs of concept in circulation, "of not fully verified effectiveness". The official advisories do not say they are public: I am reporting that as CERT-AGID's claim, I have not verified it. It changes little: you update anyway, and now.
If you want a quick check from the outside without opening a terminal, I built a free PHP version check: it reads what your site declares and tells you whether it is patched. It cannot see everything, so the commands below are still the real check.
First question: which PHP is actually running
From a shell:
php -v
php -r 'echo PHP_VERSION, " ", PHP_SAPI, PHP_EOL;'
Careful: this gives you the version and SAPI of the command line, which is cli. The PHP serving your pages can be a different binary with a different php.ini. On an FPM server, ask it directly:
php-fpm -v
php-fpm -i | grep -i upload_progress
The binary name depends on the distribution (php-fpm8.3 on Debian and Ubuntu, for instance). In a Docker container: docker compose exec app php-fpm -v, where app is your service name.
From the outside, many servers just tell you:
curl -sI https://yoursite.example | grep -i x-powered-by
If the response contains something like X-Powered-By: PHP/8.x.y, your server is announcing its exact version to everyone. On the day a CVE lands, that is the first filter anyone hunting for targets uses. Turn it off with expose_php = Off in php.ini. It is not a defense, a vulnerable version stays vulnerable, but there is no reason to give the information away.
Second question: mod_php or FPM
The SAPI changes the risk, so check it on the web PHP, not on the CLI. The most direct way is a temporary file with an unguessable name, deleted as soon as you have read it:
<?php
echo PHP_VERSION, ' ', PHP_SAPI, ' upload_progress=', ini_get('session.upload_progress.enabled');
apache2handler means mod_php. fpm-fcgi means PHP-FPM. The same value shows up as "Server API" in phpinfo(), which exposes a lot more: if you use it, remove it right away.
The temporary mitigation, and its limits
The bulletin lists session.upload_progress.enabled = 0 as a temporary measure. The official advisory is more precise: it applies to CVE-2026-109619, blocks the demonstrated attack path through the session and not the others, and needs a worker restart to take effect. For CVE-2026-109618 the advisory suggests different measures, worth reading there. Every source agrees the only complete fix is updating.
If you need a few hours before you can update, turn it off and restart FPM or Apache. If your code uses the session-based upload progress bar, that will stop working: better to know in advance.
The container case: FROM php:8.x-fpm does not patch itself
The service with the problem was a Docker application whose Dockerfile starts from a branch tag, something like:
FROM php:8.x-fpm
The common belief is that a branch tag like 8.4-fpm or 8.3-fpm keeps you current. That is only half true. On the registry the tag moves to the latest patch (official images with the fixed versions were available by the evening of the 21st). But on your server Docker uses the image it has cached, pulled on the day of the last build. Until you rebuild, the container keeps running the PHP from weeks ago, whatever the Dockerfile says.
And a plain docker compose build is not enough: without --pull it reuses the local base image. What you need is:
docker compose build --pull
docker compose up -d
docker compose exec app php-fpm -v
The last command is the check: until you see the fixed version, you are not done. For services that run a prebuilt image with no build step, the equivalent is docker compose pull followed by up -d.
To stop relying on memory next time, two approaches work well together:
- Pin the patch in the
Dockerfile, for exampleFROM php:8.4.27-fpm. The running version becomes the one written in the repository, and an update becomes a commit you can see in the history. - Let Renovate or Dependabot propose updates. They read the
FROMline and open a pull request when a new patch ships. The rebuild goes through the pipeline instead of depending on someone remembering it.
A container gives you reproducibility, and reproducibility includes the old PHP, faithfully reproduced until you change it. If you run Laravel in Docker, I also wrote about queue timeouts and crashes in containers.
On shared hosting
Here the work belongs to the provider, but the check is still yours. Many shared hosts still run Apache with mod_php, which is the most exposed case. In the control panel, find the PHP version selector: check that the branch is supported and the patch level is the fixed one. If the panel only shows "8.3" without the patch, the temporary file above gives you the exact version. If it is still old after a day or two, contact support and cite both CVEs.
Checklist
- List every service that runs PHP, including the forgotten ones: staging, internal panels, old sites.
- For each one, read the version and SAPI of the web PHP, not the CLI.
- If it is below 8.5.12, 8.4.27, 8.3.36 or 8.2.35 on its branch, update. If it is 8.1 or older, plan the move to a supported branch.
- In Docker:
build --pull,up -d, then check the version inside the container. - If you cannot update right away:
session.upload_progress.enabled = 0, restart the workers, update as soon as possible. - Set
expose_php = Offand check thatX-Powered-Byis gone. - Delete any temporary
phpinfo()files. - Pin the patch in
FROMand enable Renovate or Dependabot, so next time a pull request shows up.
The check took me less than an hour. The vulnerable service was not the one I would have thought of first: it was the one that had been running fine for weeks and that nobody touched.