On the evening of 20 October 2026, a post started circulating claiming that two critical remote code execution vulnerabilities in PHP — not in a framework or a plugin, but in the language runtime itself — would be disclosed within days. Nearly every website we run for our customers is built on PHP. This is an account of the next 48 hours: what we did, in what order, and what we got wrong.
From rumour to official notice
The first post was unverified, so the first job was to find out whether it was real. Within hours the PHP security team published an advance notice: two critical vulnerabilities, rated CVSS v4.0 9.2, with a security release scheduled for 21 October at 14:00 UTC — moved a day earlier because details had leaked. The fixes actually landed at about 13:34 UTC. Until then nobody outside the PHP project knew which versions or configurations were affected, so we planned for the worst case: every PHP version on every server.
We started by building an inventory — every server, every installed PHP version, every pool that serves a site, and which package source each one updates from. That inventory turned out to matter more than any single fix. It showed us that one older server ran an operating system release whose package source had stopped publishing updates altogether, which meant no patch would ever arrive there through the normal channel.
Layered measures before the patch
With no patch yet, we added protection in layers, each cheap and reversible:
- Disabling the suspected entry point. A researcher who reproduced the bugs pointed at PHP's session upload-progress handler. We set
session.upload_progress.enabled = Offon every PHP version on every server, after checking that no site relied on it. The official advisory later confirmed that this path was exploitable on PHP-FPM — and also that it was only one of several paths, so we never treated it as a fix. - Looking for traces of an attack. We swept every server read-only: PHP worker crashes (a memory-corruption attempt typically crashes a worker), shells or downloaders started by PHP, changed PHP files compared against what we had deployed, new cron jobs, users and SSH keys. We found nothing.
- Locking the origin. Most sites sit behind Cloudflare, whose emergency rules can filter attack traffic — but only if the traffic goes through Cloudflare. Anyone who knows a server's IP address can skip it. We changed the web servers so that sites behind Cloudflare only accept connections from Cloudflare and from the server itself, and moved the few sites that were not yet behind Cloudflare onto it. We wrote up how to do this in a separate guide.
The fix, and the mirror that would not serve it
When the release came out, the details were sobering: CVE-2026-109618 corrupts memory when PHP runs out of memory while parsing a request, and CVE-2026-109619 lets state from one request leak into the next on the same worker. Both allow unauthenticated code execution. Fixed versions are 8.2.35, 8.3.36, 8.4.27 and 8.5.12, with a minimal backport for 7.4. Our security advisory has the technical details.
The patched packages were built within hours, but the package mirror everyone downloads from returned HTTP 503 for most of the evening — the whole world was pulling the same update at once — and kept serving an outdated index. Rather than wait, we downloaded the already-published packages directly from the build service and installed only the packages each server already had, keeping every site on its existing PHP version. By the end of the evening every PHP version on our web servers — 7.4, 8.2, 8.3, 8.4 and 8.5 — was running a patched release, and we restarted the long-running PHP workers too, so nothing kept the old code in memory. One containerised component, whose build needs the PHP extension repository, had to wait: that repository was overloaded too, and the component is rebuilt as soon as it can be.
Moving an end-of-life server
The server whose operating system had stopped receiving updates needed more than a patch. We took a full, verified backup off the server, upgraded it to a supported release the same evening, and reinstalled its sites' PHP version on the new system. This was the only part of the incident our customers could see: that server's sites were offline for about 30 minutes. Apart from a few seconds during service restarts, every other site stayed up.
Three mistakes
We made mistakes, and they are as instructive as the parts that went well.
- We removed a runtime that was in use. Early on we removed an old PHP version from a server because nothing appeared to use it. We had checked cron jobs and scripts, but not service definitions — and a monitoring agent ran on it. It kept working from memory, so nothing broke, and our own attack-trace sweep caught it hours later; we reinstalled it. The lesson: check service definitions before removing anything.
- We restarted the manager instead of the apps. After updating Node.js we restarted not just the applications but the process manager running them. It reloaded an outdated saved process list, and an internal service was down for about ten minutes until we restored it. The lesson: restart the applications, not the manager.
- The OS upgrade installed something we did not ask for. The release upgrade quietly installed a web server module that took the port our real web server needed. Our post-upgrade checks caught it before we called the upgrade done, and we removed it — which also mattered for security, since that module is the most exposed way to run PHP for this exact bug.
What else we fixed while we were there
The same days brought two other security releases. A certificate authority's intermediate keys were found to be compromised; the certificate lists on our servers were updated, and we checked that none of our sites used that authority. Node.js shipped security releases, which we installed and restarted our Node services onto. We also bound databases and caches to the local machine only — the firewall already blocked them, but during the OS upgrade the firewall was briefly off, and defence in depth should not depend on one layer. And we turned on process auditing, so that the next time we ask "did PHP run a command it should not have?", the answer is in a log rather than a guess.
What we take away
A fix being released and a fix being installed are different moments, sometimes hours apart. The hours in between are where preparation pays off: knowing what you run, having cheap interim measures ready, and being able to install a patch without waiting for the usual channel. We are keeping the inventory, the auditing and the origin lock as permanent changes.