How to patch PHP in a hurry without breaking production
PHP ships an out-of-band fix for two critical flaws at 14:00 UTC today. Here is the hour of preparation that turns the patch into a non-event, and the runbook for when it lands.
Contents
PHP’s security team has scheduled an out-of-band release for 14:00 UTC today, 21 October, fixing two vulnerabilities both rated CVSS v4.0 9.2. I wrote that up as it broke in the Wire brief.
At the time of writing the fix is not out. That is the useful part. You have a few hours of warning, which almost never happens with a security release, and how you spend them decides whether this afternoon is twenty minutes of work or a long evening.
What is actually known, and what is not
Worth separating, because a lot of what is circulating is neither.
Known, from the PHP Foundation’s own announcement: two vulnerabilities, both CVSS v4.0 9.2, release scheduled for 14:00 UTC on 21 October. The team says it may ship earlier if details leak or if it sees exploitation in the wild.
Not known: the CVE numbers, the affected versions, the affected components, and whether your configuration is exposed at all. Researchers have linked the pair to two CVE identifiers based on a leak. PHP has confirmed nothing, and neither should you.
That second list matters more than it looks. You cannot write a targeted mitigation for a vulnerability whose mechanism nobody has published. Anyone telling you today to disable a specific function or add a specific WAF rule is guessing. The only real mitigation is being ready to patch quickly.
The hour before
Everything here is worth doing whether or not this particular release turns out to affect you.
1. Find every PHP you are running
Not the one you think of. All of them. The list is always longer than people expect: web servers, the CLI that cron calls, queue workers, a legacy VM somebody set up in 2019, CI images, a Docker base image three layers down, the Homebrew PHP on a developer laptop that has production credentials in a .env.
On a host:
# Every PHP binary the system knows about, and what each one reports
for b in /usr/bin/php* /usr/sbin/php-fpm* /opt/*/bin/php; do
[ -x "$b" ] && printf '%-32s %s\n' "$b" "$("$b" -v 2>/dev/null | head -1)"
done
Across a fleet, the crude version beats no version:
while read -r host; do
printf '%-28s %s\n' "$host" "$(ssh -o ConnectTimeout=5 "$host" 'php -v 2>/dev/null | head -1' || echo UNREACHABLE)"
done < hosts.txt
2. Find out where your PHP comes from
This decides how fast you can patch, and it is the question people most often cannot answer under pressure.
| Source | Who ships the fix | Typical lag |
|---|---|---|
| Distro packages (Debian, Ubuntu, RHEL) | Your distro’s security team | Hours to a few days |
| Sury (Debian/Ubuntu) or Remi (RHEL) | Repository maintainer, tracking upstream | Usually same day |
| Official Docker images | Docker Official Images, after upstream | Same day, then you rebuild |
| Compiled from source | You | However fast you are |
dpkg -l | awk '/^ii php/ {print $2, $3}' # Debian/Ubuntu
rpm -qa 'php*' --qf '%{NAME} %{VERSION}\n' # RHEL family
If the answer is “compiled from source by someone who left”, today is the day that becomes a problem. Find the build script now, not at 14:05.
3. Prove you can restart safely
This is the step people skip, and it is the one that causes the outage.
Upgrading the package does not change the PHP that is serving requests. The FPM master holds its binary open, and opcache holds compiled bytecode in memory. Until you restart php-fpm, you have patched your disk and nothing else.
# What the running FPM master is actually executing
readlink -f "/proc/$(pgrep -o php-fpm)/exe"
So, before the release: confirm you know the service name (php8.3-fpm, php-fpm, something custom), confirm a restart does not drop in-flight requests, and confirm you know what else needs bouncing.
4. Know your rollback
Which exact version are you on right now, and can you reinstall it? Write it down.
apt-get install --only-upgrade --dry-run php8.3-fpm # see what would change
The runbook, once it lands
Verify before you act
Check php.net and the official announcement. Not a tweet, not an aggregator, not a CVE database that has front-run the advisory. The release will show up in the changelog and on the downloads page.
Read which versions are affected. If the flaw is in an extension you do not build or a SAPI you do not run, your urgency changes.
Patch
Debian and Ubuntu, upgrading every installed PHP package rather than guessing at names:
sudo apt-get update
apt list --upgradable 2>/dev/null | grep -i php # look before you leap
sudo apt-get install --only-upgrade -y $(dpkg -l | awk '/^ii php/ {print $2}')
RHEL, Alma, Rocky:
sudo dnf --refresh update 'php*'
Then restart what holds PHP in memory. This is more than the web server:
sudo systemctl restart php8.3-fpm # the SAPI, not nginx
php artisan queue:restart # workers finish the current job and exit
php artisan horizon:terminate # supervisor brings them back on the new binary
php artisan octane:reload # Octane, Swoole and RoadRunner keep PHP resident
Anything long-running keeps the old binary until it is told otherwise: Octane, RoadRunner, Swoole, FrankenPHP, supervisor-managed workers, and any daemon you wrote yourself. Reloading nginx does nothing at all here, because nginx is not running your code.
Verify the thing that serves traffic
Checking php -v verifies the CLI. The CLI is not what answers requests.
# the version FPM is actually running, after the restart
readlink -f "/proc/$(pgrep -o php-fpm)/exe"
php-fpm8.3 -v | head -1
Better, check through the front door with a small endpoint you keep behind an allowlist:
<?php // /internal/phpversion, restricted by IP or auth
header('Content-Type: text/plain');
echo PHP_VERSION, "\n", PHP_SAPI, "\n";
curl -s https://app.internal/internal/phpversion
Containers are a different job
Do not apt-get upgrade inside a running container. It works until the next deploy silently reverts it, which is the worst possible failure mode: patched on Tuesday, vulnerable on Wednesday, and nobody notices.
Rebuild and redeploy instead.
# A tag, not a digest, so a rebuild picks up the rebuilt base image
FROM php:8.3-fpm-bookworm
docker build --pull --no-cache -t registry.example.com/app:$(git rev-parse --short HEAD) .
docker push registry.example.com/app:$(git rev-parse --short HEAD)
kubectl set image deployment/app app=registry.example.com/app:$(git rev-parse --short HEAD)
--pull is the part that matters. Without it you rebuild on the cached base layer and ship the same vulnerable PHP with a new tag.
If you pin base images by digest, and you should for reproducibility, then you need a process that bumps the digest. A pinned digest is a promise that nothing changes, including the fixes.
If you are on 8.1 or older
Upstream PHP will not ship you a fix. 8.1 left security support at the end of 2025, and anything older went long before that.
But “unsupported upstream” is not the same as “no patch”, and this is where a lot of advice gets it wrong. If you run the PHP that came with your distro, your distro’s security team very likely backports the fix into their older version. Ubuntu 22.04 ships PHP 8.1 and Canonical patches it. RHEL does the same for its module streams. Check your vendor before concluding you are stranded.
You are genuinely stranded if you are on an unsupported PHP from a third-party repository that has stopped building it, or a source build nobody maintains. In that case the honest options are to upgrade the branch, or to accept a known-unpatched CVSS 9.2 on an internet-facing service. There is no third option, and the second one is not really an option.
If you cannot patch today
Reduce what an attacker can reach, since you cannot block something whose mechanism is unpublished.
- Check what is actually exposed. Admin panels, staging copies, old subdomains, the forgotten box running an unmaintained app. The fastest risk reduction is usually taking something off the internet rather than patching it.
- Let your WAF vendor help. Cloudflare, AWS and the rest frequently ship a managed rule within hours of a serious PHP flaw. Make sure managed rulesets are on and set to auto-update. That is a stopgap, not a fix.
- Turn up logging and keep it. If you are patching late, you want the ability to answer “were we hit before we patched” afterwards. Retention is cheaper than the alternative.
Make the next one cheaper
There will be a next one. The work that pays off is not this afternoon’s patching, it is removing the parts that made it stressful.
- An inventory you do not have to build by hand. Even a nightly job writing hostname and PHP version into a file beats twenty SSH sessions.
- Unattended security upgrades on anything where a restart is survivable.
unattended-upgradeson Debian and Ubuntu,dnf-automaticon RHEL, scoped to the security repository. - A base-image rebuild pipeline. A weekly scheduled build with
--pull, so an emergency rebuild is a path you have walked rather than one you are discovering. - A rollback you have actually tested. Not a plan. A thing you have done.
None of that is interesting work. All of it turns the next 14:00 UTC announcement into a calendar reminder instead of an evening.
I will update the Wire brief once the release is out and the affected versions are known.