Introduction
Seven hours after PHP shipped the emergency fix for PHP2Shell, a public scan of roughly 606,000 internet-facing services on supported PHP branches found 2,149 running a patched version. That is 0.36%. The patch was free, the advisory was everywhere, and almost nobody had it running yet.
TLDR
PHP2Shell is the nickname for CVE-2026-109618 and CVE-2026-109619, two memory safety flaws in the PHP engine that both score 9.2 on CVSS v4. Updating the package is the easy half. The fix takes effect only after every PHP worker restarts, and a package scanner cannot see inside a running worker. This post covers the five places a PHP patch quietly fails, the six checks that settle it, and what to write down so the answer survives your next deploy.
Why “we patched it” is not an answer
Patching PHP writes new files to disk. It does not touch processes that are already running. A worker that started before the upgrade keeps serving the code it loaded at startup, and it will keep doing that until something restarts it.
That means two servers can report the same version and have completely different exposure. One restarted its pools. One did not.
PHP says it plainly: updating is the only complete fix, and no configuration change makes an installation safe. Memory limits and the session upload progress setting reduce one path each. Neither is remediation.
Five places a PHP patch quietly fails
Five places a PHP patch quietly fails
Each one leaves the package database saying you are fixed. Each one has a check that settles it.
systemctl restart php8.3-fpm
php -v ≠ the web version
dpkg -l | grep php
docker exec <id> php -v
rpm -q php-cli --changelog
One rule covers all five. Verify from the path an attacker would use, which is a request arriving at a live worker, and not from the record of what you installed. Configuration toggles such as memory limits or the upload progress setting reduce one path and are not remediation.
Every one of these leaves your package database saying you are fixed.
1. The worker never restarted
FPM pools and Apache children survive a package update. Until you reload or restart the service, the workers keep serving the old build.
2. You checked the shell, not the site
FPM reads its own pool configuration rather than the command line settings. So a terminal check can show a patched version while the site still serves the old one. The version that matters is the one a served request reports.
3. The host runs more than one PHP
Hosting panels and multi-site servers commonly keep several branches installed. A patched 8.2 does nothing for a site still routed to an 8.1 pool. CMS fleets collect orphaned PHP versions nobody owns.
4. The image was never rebuilt
A running container keeps the PHP it was built with, so updating the base image alone changes nothing. Patch the container, and the image in your registry is still exposed, waiting for the next deploy.
5. The version number is lying
Distribution backports fix the code and keep the old version string. This cuts both ways. A server reading 8.1.33 may be fixed, and a server reading a higher number may not be. Trust the package changelog, not the banner.
Six checks that settle it
- Read the version from a request PHP actually served, not from a shell.
- Search the package changelog for both CVE IDs rather than comparing version numbers.
- Compare worker process start time against package install time on every pool.
- List every PHP branch installed on the host, then confirm which binary each site is routed to.
- Rebuild container images from a patched base, redeploy, then confirm the orchestrator rolled the new image.
- Re-test the exposed surface from outside against a target you own.
Why the honest test is disruptive
Here is the part most remediation guides skip. The definitive external test for PHP2Shell sends a large payload designed to exhaust worker memory. It can crash a worker and interrupt live requests.
So the only test that truly proves the fix carries a real cost. Published guidance says to run it outside peak hours, and that a clean result from a passive checker does not confirm you are patched.
This is why patch verification is a governance problem, not just a technical one. Somebody has to decide which production hosts get tested, inside which window, and who can stop the run. Without that, teams skip the test and write patched in the ticket.
What to write down
A patch ticket closes. An evidence record stays open until something proves it wrong. For each host or image, record the fixed version and how you confirmed it, the restart time, the web served version, whether the host is reachable from outside, what you tested and what you did not, any observed probe activity with its time window, the owner of the next action, and what would invalidate the record.
Any field may say unknown. It then has to name the missing evidence.
A worker restart is a deployment change, so the next deploy should send the record back to recheck required rather than leaving it green. For hosts you cannot patch yet, write the compensating control and an approved exception with an expiry date.
Nine fields, and “unknown” is a legal answer
A patch ticket closes. An evidence record stays open until something proves it wrong. Here is one host, written the way an auditor or a board can read it.
Why the unknowns matter most. A record with no unknown fields is usually a record that stopped asking. Naming the missing evidence is what turns a patch log into something you can defend to an auditor, a regulator or a customer.
Who goes first
- Apache servers running mod_php are the worst case because the attack reaches the engine before a single line of application code runs. PHP rates that setup as highest risk, and one scan counted at least 565,000 public services using it.
- Internet-facing Linux web servers with no endpoint monitoring come next. A successful run is reported as near-silent: the worker doesn’t crash, and the response looks normal, so there may be nothing in the application log to find later.
- Shared hosting and multi-tenant panels follow, because one missed pool affects every site on the box.
- End-of-life branches are a project, not a patch. PHP 8.1 and older get no official fix, so the choices are a vendor backport, a migration, or isolating the host while you plan one.
Patched? Prove it.
Verification is paperwork nobody budgets for, and it lands on the same people who just spent a night patching.
- Secure.com Red Teaming retests your exposed PHP surface after you patch, inside a scope your team signs and can stop mid-run.
- The one-target scan at $5,000 includes a 30-day verification re-scan, so proving the fix isn’t a second purchase.
- CSPM Teammate maps the attack path on your public hosts once your scanner reports the finding and the NVD record lands, with no wait for a KEV listing.
- Response workflows hold for a named approver in Slack or Teams before anything restarts, isolates, or rotates.
- Every run leaves one record per host: what was tested, what was not, and what is still unknown.
Your team sets the rules. The teammates do the work. Attack. Harden. Prove. Repeat.