On 21 October 2026 the PHP project shipped an out-of-band security release for two critical vulnerabilities in PHP core, CVE-2026-109618 and CVE-2026-109619, known publicly as “php2shell”. Both are rated 9.2 on CVSS v4.0, and both can lead to remote code execution. The details leaked before the coordinated disclosure, a proof-of-concept tool was already circulating, and security firms warned that mass scanning would start within fifteen minutes of the patch going live. Through the day, organisations took PHP sites offline as a precaution rather than wait for it.
I was up for this one. We build this kind of defence for a living, from the static edge and the Web Application Firewall to Quant Sentinel, our AI penetration tester, so a day like this is a working night for us rather than a headline we read about afterwards. The release was due at 14:00 UTC, and before it landed we had already put what mitigations we could in front of our customers, because the details had leaked and I did not want to be reading the advisory for the first time while scanners were already running. When the source tags appeared, the fix being out changed very little for the next few hours, for reasons that had nothing to do with us.
The window has closed to minutes
For most of my career the defender had time on their side. A patch shipped, and a working exploit took hours or days to follow, because turning a fix into a reliable attack was skilled work. That lag, between the fix being available and an exploit existing, is where patching did its job. With php2shell the lag was close to zero. The pattern going round on the day was that you could take the leaked patch diff, hand it to a language model, and get a working exploit back in about the time it took to read the diff, because the diff points straight at the bug and the model writes the code that used to take an expert. So the fix and a usable exploit were available at the same moment. Patching quickly still matters, but it no longer buys you a head start, because the attacker works from the same diff you do and needs less time with it than you need to rebuild and redeploy.
The debt underneath “just patch”
“Just patch” is good advice that hides a lot of work, and it assumes two things that were not true for everyone on the 21st: that a patch exists for your version, and that you can get it running before the scanners arrive.
The first assumption fails at end of life. PHP 7.4 is out of support and will not get this fix, and we have real customers who are not off it yet. For them there is no patch to apply, only mitigation and a migration that is still in progress.
The second assumption is the one most people underestimate. A patched PHP is not a file you drop in place. For most of the ecosystem it is a container image that someone else has to build first. php.net published the source fix and the advisories at 14:00 UTC, and the patch diff was public within the hour. The image most people actually deploy, the official docker-library php image, was not usable for another two hours and forty minutes. For part of that window the version tags had been pushed as empty manifest indexes, so the tag existed with nothing behind it, while the floating tags still served the old, vulnerable 8.4.26. If you tried to rebuild during that window you got one of two results: FROM php:8.4-apache gave you the old version and a build that went green, or FROM php:8.4.27-apache-trixie failed outright because there was no image behind the tag to pull. A separate GitHub incident, with Actions and webhook deliveries degraded from 14:52 UTC, ran through the same window and overlapped the publish pipeline, though I cannot say with certainty how much it contributed.
That one image sits underneath a very large part of the web. The official WordPress image builds on it, major CMS distributions build on layers that build on it, our own base builds on it, and so do a long tail of custom Dockerfiles. Some widely used government and enterprise stacks sit two or three steps further down that chain, and were several hours further behind again. So for a few hours a single volunteer-run build pipeline, slowed by an unrelated outage, was most of what stood between a disclosed 9.2 and the patched runtime reaching production across a large part of the internet.
Here is how that gap actually ran, in UTC, from the first sign of a public leak to the patched image finally being deployable.
01:55 UTC — a leak, and a live confirmation
The first signs of a public leak appear. We replicate it against a running image and confirm an unauthenticated, default-config remote code execution. A crafted multipart POST to any URL, with no login and no upload form, is enough.
Ahead of the fix — mitigations in front of customers
We ship a platform WAF rule that blocks the trigger, and a php.ini mitigation for the runtimes we build, so customers are covered hours before an official patch exists.
14:00 UTC — the fix ships
php.net publishes the source fix and the advisories. The fix lands in 8.2.35, 8.3.36, 8.4.27 and 8.5.12, and the patch diff is public within the hour.
14:19 UTC — the image bump merges
The official PHP image repository merges the version bump. The fix is now in source for the image most of the ecosystem deploys, but not yet in anything you can pull.
14:52 UTC — an unrelated outage
A GitHub incident begins. Actions and webhook deliveries degrade, and the image build-and-publish pipeline runs straight through it.
15:16 UTC — built, but empty
The official image build is triggered, but the new version tags sit as empty manifest stubs for about ninety minutes. The tag exists with nothing behind it.
16:41 UTC — finally pullable
The floating tags repoint to the real patched images, pull-verified a few minutes later. About two hours and forty minutes from a fix in source to an image you could actually deploy.
17:09 UTC — the next link in the chain
A widely used base-image layer downstream of the official image merges its own PHP bump, starting the next stage of the chain that many managed platforms depend on.
18:27 UTC — still building
That layer is still building its images. The managed platforms built on top of it, further down the chain again, are realistically still hours from a patched runtime.
It is worth stopping on that, because it is easy to treat a supply chain as infrastructure and forget that it is people. The image pipeline most of the web was waiting on that afternoon is run by volunteers. Somewhere in the world, at some unsociable hour, actual humans were rebuilding by hand while an unrelated outage fought them the whole way. My heart goes out to them, and to the on-call engineers and the maintainers further down every chain like this one. I am not writing this from the sidelines or a high horse. I have been in it for more than twenty-four hours straight on this one, patching base images, standing up mitigations and pushing application deployments across thousands of sites, and I am one of many who spent the day the same way. Their work is getting harder in a way that is not their fault. Wave after wave of CVEs, and now an adversary with AI on its side that turns each disclosure into a working exploit faster than anyone can ship the fix. It is bloody hard right now, and most of the people holding the line are tired, under-resourced, and doing it anyway.
Static sites have nothing for this to hit
This whole class of flaw has one precondition: PHP has to parse an incoming request. php2shell triggers while PHP reads a multipart upload, before any application code runs. A static site never does that. It serves files that were rendered ahead of time, so when a request arrives there is no PHP process parsing it, and a PHP memory-corruption bug has nothing to reach. The vulnerability can sit in the binary and still have no way in.
That is the model Quant is built on, and it reaches well beyond any one CMS. We make Drupal and WordPress static, we build and serve the popular static site generators, and we take static output from whatever a team already works in. The tool behind the site keeps its full authoring and build workflow, and the site the public hits is a set of pre-rendered files served from our edge. For a flaw like php2shell, there is no public attack surface at all, because the public side is not running PHP.
There is one honest qualification. The authoring and build side still runs PHP somewhere, so that is where your dynamic exposure lives and where it should be protected. What goes to zero is the surface facing the internet, which is the surface that gets mass-scanned fifteen minutes after a release.
The sharpest version of this played out on our own platform during the incident. We have customers still running PHP 7.4, and some still on Drupal 7. Both are years past end of life, and neither will ever get a fix for this. On a public-facing PHP host they would have been the most exposed systems in the fleet, the textbook example of what gets owned first. On ours they were not exposed at all, because none of their code runs PHP on a public network. The public side of those sites is static files on the edge, and the old, unpatched runtime only ever runs on the authoring side, off the public internet entirely. The customers whose sites we serve statically had nothing to do that night. The ones with dynamic PHP origins, ours included, had a long one.
For the parts that stay dynamic
Not everything can be static, and real applications on Quant Cloud do run PHP, built and deployed from our customers’ own container images in whatever stack they choose. For those, the same event is a useful demonstration of defence in depth. We run the Web Application Firewall ourselves, so a rule we ship protects traffic immediately rather than after each customer redeploys. Where a runtime cannot be patched yet, including end-of-life PHP 7.4 which gets no official fix at all, we can take the vulnerable request type out of the path entirely by denying multipart uploads at the web server, before PHP reads them. That removes the trigger and buys time while the patched runtime rolls out underneath. There is a bigger lever when you want the surface gone rather than just narrowed. Static failover keeps a static clone of a dynamic site ready at the edge, and you can flip to it automatically or on demand, so for the length of an incident the public is served pre-rendered files and no PHP runs in the request path at all. It is the fastest way to get the static guarantee without re-architecting anything, which buys you room to patch, or to review the architecture and flatten the stack, with the site not exposed while you do. For supported PHP the patched version is the real fix, and that is what we have now rolled across the applications we operate.
Where the bug lives, and why one fix was only half a fix
Two different code paths matter here, and they decide which mitigation actually works. The session feature that everyone talked about is one path. The multipart parser that runs before it is the other.
| Code path | Where it lives | What arms it | What covers it |
|---|---|---|---|
| The session callback | ext/session/session.c | A multipart field named PHP_SESSION_UPLOAD_PROGRESS, with session.upload_progress on (the default) | The session.upload_progress.enabled=0 ini flag closes this path completely |
| The multipart parser | main/rfc1867.c | Any multipart body the parser reads, before the application routes the request | The ini flag does not cover this. Only blocking multipart, or the patch, removes it |
Disabling session.upload_progress was the early advice, and for the session path it is a complete fix. But the exposure reaches the parser itself, which runs before any of that, so the ini flag is a mitigation that happens to hold rather than one you can rely on. That distinction is the whole game during an incident. If the parser is in play, only taking multipart out of the request path, or running patched PHP, actually removes the bug.
What stops it, and how fast
| Mitigation | Speed | Coverage | The catch |
|---|---|---|---|
session.upload_progress.enabled=0 in php.ini | Minutes to write, then an image rebuild and deploy | The session path only | Partial if the parser path is in play, and it still needs a rebuild |
Block multipart/form-data at the edge or web server | Instant, fleet-wide | Removes the trigger entirely | Costs file uploads while in place, and depends on full-body inspection |
| Patched PHP (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12) | Hours to days, gated by the supply chain | Complete | You wait for the image to exist, then rebuild and deploy |
| A static edge, with no PHP in the request path | Already in place | No public attack surface at all | Protects the public read path, not the authoring and build side |
The two fast options both work by making sure the request never reaches vulnerable code. Blocking multipart/form-data at the edge is instant and fleet-wide, and the only thing it costs you is file uploads while it is on:
# Temporary mitigation for CVE-2026-109618 / 109619. Revert after patching.
if ($content_type ~* "^multipart/form-data") {
return 403;
}A WAF rule can be more surgical, blocking only the request that carries the trigger field rather than every upload. This is the rule we shipped across the platform ahead of the fix, hardened so the field name is caught whether it is quoted, unquoted, or buried in a multipart part header:
SecRule REQUEST_HEADERS:Content-Type "@rx ^multipart/form-data" \
"id:2026109618,phase:2,deny,status:403,log,t:lowercase,chain,\
msg:'Block PHP session.upload_progress RCE trigger (CVE-2026-109618/109619)'"
SecRule ARGS_NAMES|MULTIPART_PART_HEADERS \
"@rx (?:^|(?i:name)\s*=\s*[\"']?)PHP_SESSION_UPLOAD_PROGRESS(?:[\"'\s;\r]|$)"There is a well-known weakness in any WAF rule like this, and it is worth being honest about. A WAF that inspects only the first part of a request body can be bypassed by padding the body so the trigger falls past the inspection window, while PHP still parses the whole thing. The rule is only as strong as the body inspection behind it. On our edge that gap is closed by configuration rather than by hope: the request body limit is enforced with a reject action, so a body large enough to hide the trigger is refused outright instead of being scanned in part and passed through. The WAF buys time across the whole fleet in seconds. The patched runtime is still the fix underneath it.
What this means for you
Applying patched PHP is the first priority. The fixed versions are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. The platform protections reduce exposure, and upgrading to a patched PHP version is the only measure that fully resolves the vulnerability, because the flaw is an engine-level one rather than a single feature that can be switched off.
If you run a PHP application on Quant Cloud using our Apache/PHP base image on a supported version (8.2 or newer), the patched base images are now published — 8.2.35, 8.3.36 and 8.4.27. Rebuild and redeploy to pick up the fixed PHP. We have already rebuilt and redeployed the applications we operate; a custom application you build and deploy yourself needs its own rebuild and redeploy to pull the patched base.
If you run PHP 7.4 on Quant, we block multipart/form-data uploads by default, because 7.4 is end of life and will not get the fix. Page loads, logins and standard form submissions keep working; file uploads do not. Plan to upgrade to a supported PHP version. If you need uploads working on 7.4 in the meantime, set the environment variable QUANT_ALLOW_MULTIPART=true, which re-opens the vulnerable path on an unpatched runtime.
If you host PHP outside Quant or build your own container images, upgrade to a patched PHP version as soon as you can, because that is the only complete fix. As interim cover, deny multipart/form-data requests at your web server or CDN before PHP parses them, which removes the known trigger. Disabling session.upload_progress circulated as an early workaround, but the patch showed the flaw is broader than that one feature, so treat it as partial at best.
If you are unsure how your site is configured or which PHP version it runs, contact our support team and we will check it with you.
This is bigger than PHP
None of this was the last time, and none of it is really about PHP. The same thing is now true of every package in your stack. A disclosure lands, the diff is public within the hour, and a language model turns that diff into a working exploit before most teams have finished reading the advisory. The skill that used to sit between a patched bug and a weaponised one has been automated away, and it is not coming back. This holds for your npm tree, your Python dependencies, your base images, and the long tail of transitive packages nobody on your team has ever read.
The approach has to change, and I think it changes in three ways.
The first is that you need defences that move at the speed of the attack. When a working exploit exists within an hour of disclosure, a patch cycle that waits on a human is already too slow for the first wave. Detection and response that uses AI to triage and act, a WAF that ships a rule across the whole fleet the moment a trigger is understood, and automated assessment of where you are actually exposed are no longer optional extras. That last one, finding the exploitable path on your own systems before an attacker does, is why we built Quant Sentinel, an AI penetration tester that probes your applications the way an attacker would and runs in your pipeline rather than once a year. The attacker is already using these tools, so your defence has to use them too.
The second is that you need to flatten your stack. Every layer you run is a layer that can carry a critical bug on a day you did not choose, and every layer is attack surface as much as it is running cost. The fewer moving parts between a request and a response, the fewer places an exploit like this has to land, and the faster you can reason about your exposure when the next one drops.
The third is that you need to go static wherever you can. This is the one I have argued for the longest, back when the case was mostly about speed and cost. The security case is now the strongest of all. A static edge runs no PHP and no application runtime, so a memory-corruption bug has nothing to reach. The vulnerability can sit in the binary and never have a way in, because the public side of the site is files served from the edge, decoupled from the origin that does the risky work. For a large share of what governments and enterprises publish, that is all the public ever needed.
php2shell itself will be a footnote by next month, but the pattern behind it will not be. Work out now which of your services would be reachable the next time this happens, and start closing that surface before it does.
About QuantCDN
Quant is a global static edge: a CDN and WAF combined with static web hosting. We make Drupal and WordPress static, support the popular static site generators, and serve static sites from whatever source a team already works in. For the parts that have to stay dynamic, Quant Cloud runs applications from your own container images behind the same edge. If you have questions about your exposure to these vulnerabilities or need help upgrading PHP, contact our support team.
Share
