
On 20 October 2026, details of two critical PHP flaws leaked a day before the coordinated release. The pair, CVE-2026-109618 and CVE-2026-109619, became known as PHP2Shell. Both score 9.2 on CVSS v4.0, neither needs a login, and the vulnerable code dates back to PHP 5.4. The PHP project shipped fixes on 21 October in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Cloudflare saw attack traffic hours before the official release.
We look after more than 500 PHP servers for our customers. They include load-balanced clusters, cPanel, Plesk and ISPConfig servers, and a long tail of older PHP versions that will never get an official fix. This post is the playbook we followed, written so that you can reuse it.
What PHP2Shell actually is
Both bugs are use-after-free flaws in the PHP engine. They start when a request hits a fatal error, usually memory_limit, part-way through native code that has not cleaned up yet. A later request on the same worker then uses the freed memory as if it were live.
- CVE-2026-109618 sits in the session upload-progress handler. It can leak worker memory through the
PHPSESSIDcookie, including passwords, session tokens and keys, and it can lead to code execution. It works even if the application never callssession_start(). - CVE-2026-109619 combines a fatal-error backtrace use-after-free with OPcache type confusion, which gives remote code execution.
Every known trigger is a multipart/form-data request. PHP parses that body before your application code runs, so application-level protections are too late. Both mod_php and PHP-FPM are affected in their default configuration.
Step 1: Inventory before you touch anything
You cannot patch what you have not counted. Within the first hour, our scripts collected the following from every server:
- Every PHP binary and FPM pool, not just
php -v. Panel servers often run four or five PHP versions side by side. - The package source for each one: distro AppStream, Remi, cPanel EA4, Plesk, Sury, or built from source.
- Whether the server sits behind a load balancer, and which pool it belongs to.
- Which PHP versions have no supported upstream branch, such as 5.6, 7.x and 8.1.
That list decided the order of work. Internet-facing, unpatched and single-node servers went to the top.
Step 2: Buy time with an interim mitigation
The interim mitigation went out fleet-wide while packages were still being built. The quickest relief was to switch off session upload progress, which closes the path that CVE-2026-109618 uses. For PHP-FPM, set it in every pool, not just the global php.ini:
; /etc/php-fpm.d/<pool>.conf (path differs per panel)
php_admin_flag[session.upload_progress.enabled] = off
Two warnings from doing this at scale:
- Verify through a web request, not the CLI. The CLI reads a different ini set from the FPM pools. We checked the live value with a short request against every pool and counted the failures.
- This is relief, not a fix. It does nothing for CVE-2026-109619. Upload progress bars in a few applications stopped working, which customers accepted for a few hours.
Step 3: Rolling updates behind the load balancer
For clustered sites the rule was simple: one node at a time, and never the last healthy one. Our scripts ran this loop per cluster:
- Put the node in drain at the load balancer, so new requests go to the other nodes and open ones finish.
- Wait until active connections on the node reach zero, with a timeout so that one stuck client cannot hold up the run.
- Update the PHP packages and reload PHP-FPM. A reload lets workers finish their requests, while a restart kills them.
- Check the node directly, bypassing the load balancer. The check confirms the patched PHP version and the session upload-progress setting through PHP-FPM, and that the application returns 200.
- Put the node back in the pool, watch its error rate for a few minutes, then move to the next node.
If any check failed, the script stopped. It left the node drained and paged an engineer instead of moving on. Capacity never dropped below N-1, so customers saw no downtime.
Step 4: cPanel, Plesk and ISPConfig servers
Panel servers have no load balancer in front, so there is nothing to drain. The safe move is a graceful FPM reload straight after the package update. Each panel needed its own method:
- cPanel: PHP comes from EasyApache 4
ea-phpXXpackages. We updated them per version and then reloaded each version’s FPM service. Accounts on CloudLinux alt-php need thealt-phppackages updated as well. - Plesk: Plesk ships its own PHP handlers (
plesk-phpXX). We updated them through the Plesk installer, then checked each handler and its FPM service, because a domain can be pinned to any installed handler. - ISPConfig: ISPConfig runs the distro PHP plus any “additional PHP versions” registered in the panel. These come from Remi, Sury or a source build, so each version was updated from its own source and its own FPM service reloaded.
On every panel server we ran a test request for each PHP version and each domain mapping. A PHP version that no site uses can still be reachable.
Step 5: Backports for PHP versions upstream no longer supports
Upstream only supports PHP 8.2 and later, so 8.1, 7.x and 5.6 get no official release. Some customers still run those versions for applications that cannot move yet. For them, we backported the upstream fix to their branch, where the affected code was close enough to the fixed code. We rebuilt PHP and tested the same request paths before rolling it out. Where the code had drifted too far for a safe backport, the customer got the interim mitigation, multipart filtering at the web server and an upgrade plan with a date on it.
Step 6: Our own RPMs, without waiting for the repos
On release day the fix was in the PHP source, but Remi and several OS vendors had not shipped packages yet. Waiting would have left internet-facing servers exposed for an unknown time. For the servers that could not wait, we built our own RPMs from the fixed source, reusing the existing spec files so that file layout, ini paths and FPM units stayed exactly the same.
The one detail that matters is version ordering. Give your build a release that sorts below the vendor’s first fixed package, for example 0.1 rather than 1. A normal dnf update then replaces your build once the official package arrives, with no pinning or manual clean-up. If you tag your build 1.el9.tp1 instead, it sorts above the vendor’s 1.el9 and silently blocks the official update.
# before deploying, confirm the official package will replace yours
rpmdev-vercmp 8.3.36-0.1.el9.tp 8.3.36-1.el9
# expected: 8.3.36-0.1.el9.tp < 8.3.36-1.el9
Step 7: Prove it, then keep proving it
A config file shows what you meant to deploy, and only a running process shows what you actually deployed. After each wave we re-ran the fleet check: the live PHP version per FPM pool through a web request, packages compared against the fixed list, and any PHP process still mapping a deleted library from before the update. Every server either passed all checks or had a named owner and a deadline.
Lessons that apply beyond PHP2Shell
- Your inventory is your response time. Most of the first hour went into finding every PHP binary, not patching it.
- Mitigate first, then patch. A partial mitigation in place within an hour is worth more than a perfect patch the next day.
- Drain, patch, verify, return. This loop, scripted and stopping on the first failure, is what makes zero downtime possible.
- You don’t have to wait for packagers. When the fix is in the upstream source, a careful in-house build with correct version ordering closes the gap safely.
Need help patching your own servers for PHP2Shell, or for the next critical CVE? Talk to our engineers, or read how we handled it for our managed customers.


