Events Join us at the AWS Summit in Dubai on 30th September
Read

PHP2Shell: How to Prove the Patch Actually Took Effect

A PHP2Shell patch can sit on disk and never take effect. Here are six checks that prove the fix is live on the worker serving traffic.

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

PHP2Shell · Patch verification

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.

1
Where it fails
The worker never restarted
FPM workers keep serving the build they started with. Updating the package on disk does not swap code inside a live process.
The check
Restart every pool, then compare process start time against package install time.
systemctl restart php8.3-fpm
2
Where it fails
You checked the shell, not the site
FPM reads its own pool configuration, not the CLI settings. A command line check can pass while the site stays exposed.
The check
Read the version from a request that PHP actually served, not from a terminal.
php -v ≠ the web version
3
Where it fails
The host runs more than one PHP
Hosting panels and multi site servers keep several branches installed. A patched 8.2 does nothing for a site still routed to 8.1.
The check
Test every installed branch, then confirm which binary each site is routed to.
dpkg -l | grep php
4
Where it fails
The image was never rebuilt
A running container keeps the PHP it was built with. Patching the container leaves the image in your registry exposed for the next deploy.
The check
Rebuild from a patched base, redeploy, then confirm the orchestrator rolled it.
docker exec <id> php -v
5
Where it fails
The version number is lying
Distribution backports fix the code and keep the old version string. The number can look wrong when you are safe, and right when you are not.
The check
Search the package changelog for both CVE IDs instead of trusting the version.
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.

What a verified host looks like on paper

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.

Host web-07.prod · PHP2Shell
Component present
PHP 8.3 via FPM, plus an 8.1 binary still installed
Confirmed
Fixed version in place
8.3.36, confirmed in the package changelog by CVE ID
Confirmed
Workers restarted
All 3 pools, 21 Oct 16:04 UTC, after the package install
Confirmed
Web served version
Read from a served request, not from a shell
Confirmed
Reachable from outside
Yes, port 443 behind the edge
Exposed
Re tested after patch
Passive check done. Active test pending a signed window
Unknown, needs scope
Not assessed
The 8.1 binary. No site is routed to it today
Unknown, named
Observed activity
14 probe requests, 21 Oct 14:38 to 22 Oct 06:00. No shell written
Reviewed
Owner of next action
Platform on call. Remove the 8.1 binary or scope it in
Assigned
What invalidates this
A deploy replaces the image, or an edge rule moves from block to count
Recheck trigger
Expiry, not closure. A worker restart is a deployment change, so the next deploy sends this record to recheck required and keeps its history. The host is never marked green on the strength of a version number alone.

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.

FAQs

How do I check if PHP2Shell is actually patched on my server?
Two checks together. Search your package changelog for CVE-2026-109618 and CVE-2026-109619, because a backport can fix the code without changing the version number. Then read the PHP version from a request the web server actually served, not from a command line, since FPM runs its own configuration.
Is restarting PHP enough, or do I need to reboot the server?
A service restart is enough, as long as it covers every FPM pool and the web server itself. Published guidance uses the terms reload or restart. Restart is the one with no ambiguity, because it guarantees no child process predates the upgrade.
My PHP version number did not change after the update. Am I still vulnerable?
Possibly not. Distribution maintainers often backport a security fix and keep the existing version string, so the number stays the same while the code is fixed. The changelog is the answer, not the banner.
Can a free online PHP2Shell checker confirm my site is safe?
No. A clean result from a passive checker does not confirm you are patched, and the definitive test sends a payload that can crash a worker. Treat an online checker as a first pass, then verify on the server and schedule a real retest.
Do I need to rebuild my container images, or is patching the running container enough?
Rebuild. A running container keeps the PHP it was built with, and patching inside it leaves the image in your registry exposed. The next deploy will reintroduce the old runtime.