Safeguard
Supply Chain Security•

PHP2Shell: How to Find and Patch Every PHP Runtime You Run

PHP2Shell playbook: find PHP on hosts, containers, hosting panels and appliances, move to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, restart workers, verify.

Safeguard Security Team
18 min read

PHP2Shell is a pair of critical flaws in PHP itself (CVE-2026-109618 and CVE-2026-109619) that affect every PHP install serving web requests. The fix is PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distro's patched package. This playbook covers finding every runtime, patching it, restarting it and proving the new code is live.

If you want the background on the bugs first, read our explainer on PHP2Shell, CVE-2026-109618 and CVE-2026-109619. This post is the hands-on part: where PHP hides, which commands tell you the truth, and the mistakes that leave a server vulnerable after everyone thinks it was patched.

What happened

The PHP project pre-announced a critical release on 20 October and shipped it on 21 October. Details leaked before the coordinated date, which is why the patch window has been so compressed.

Date (2026)EventSource
20 OctPHP security team announces a critical release for 21 Oct, 14:00 UTC, fixing two flaws rated CVSS v4.0 9.2php.net
20 to 21 OctDetails "leaked online ahead of scheduled coordinated disclosure"SecurityWeek
21 OctPHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 released; minimal source backports published for 7.4, 8.0 and 8.1php.net
By 21 OctDebian fixes php8.4 in trixie (DSA-6574-1, 8.4.27-1~deb13u1); bookworm's php8.2 still listed as vulnerable when we checkedDebian security tracker
21 OctDocker official image definitions list 8.5.12, 8.4.27, 8.3.36 and 8.2.35 tagsdocker-library/official-images

Exploitation status is disputed, so treat it as unsettled. Cloudflare's CTO said the company was seeing early attacks emerging, as quoted by The Hacker News and Tenable. watchTowr's FAQ, updated 21 October, says exploitation was observed in the wild through its honeypots. Huntress said it had not seen in-the-wild exploitation as of 10am ET on 21 October. The Hacker News reported that watchTowr had seen none as of 11:26 UTC that day, which conflicts with the FAQ, and that neither CVE was in CISA's KEV catalog at the time. SecurityWeek reports that Sansec expected mass exploitation within minutes of the patch release. None of this changes the plan: patch now.

Who is affected

According to the php.net advisory, every PHP installation that serves web requests is in scope, and the risk is highest for Apache mod_php, where one unauthenticated request can trigger the bug before any application code runs. The advisory does not say whether the CLI alone is affected. Sources also disagree on how far back the bugs go: watchTowr's FAQ lists "5 and onward" and says no patches exist for PHP 5 or 7, while php.net did publish a minimal source backport for 7.4.

Runtime or setupAffectedFixed inNotes
PHP 8.5Yes8.5.12On 8.5 only, fatal_error_backtraces=0 is a partial mitigation
PHP 8.4Yes8.4.27Debian trixie: 8.4.27-1~deb13u1
PHP 8.3Yes8.3.36Official Docker tags 8.3.36-*
PHP 8.2Yes8.2.35Upstream end of life 31 December 2026
PHP 8.1, 8.0, 7.4YesNo upstream releaseSource backports only, or a distro or LTS vendor package
PHP 7.3 and olderReported affectedNone from upstreamUpgrade or isolate
PHP-FPM, mod_php, php-cgi, FrankenPHP, LiteSpeedYesSame as the PHP branchListed SAPIs in the advisory

Severity also differs by source. PHP rates both flaws 9.2 under CVSS v4.0. Red Hat rates CVE-2026-109618 Important with a preliminary CVSS 3.1 score of 8.1 and notes that standard RHEL deployments use PHP-FPM rather than mod_php, which it says makes code execution harder in practice. Red Hat also says installing the fixed release is "the only complete fix."

How it works, in plain terms

Both bugs live in how PHP sets up and tears down per-request state. The php.net advisory describes CVE-2026-109618 as an out-of-memory condition that can interrupt PHP while request data is only partly initialised, after which cleanup treats invalid data as valid. CVE-2026-109619 is request-owned state that outlives its freed memory, so a later request on the same worker can touch it.

Two consequences matter for patching. First, the bug sits below your application, so updating WordPress, Laravel or Drupal does nothing; The Hacker News makes the same point. Second, it is about long-lived worker processes. A patched file on disk does not help a PHP-FPM master or an Apache parent that started yesterday and still has the old code mapped in memory.

What to do now

Work through these in order. Each step has a command that gives you a fact, not a guess.

1. Bare metal and VMs

Start with what the CLI says, then check every SAPI package, because the CLI and the web runtime are often separate packages.

# What the CLI binary reports
php -v
php --ini                 # which php.ini files the CLI loads
php -i | grep -E '^(PHP Version|Server API|Loaded Configuration File)'

# Every PHP binary on the box, not just the one on PATH
sudo find / -xdev -type f \( -name 'php' -o -name 'php-fpm*' -o -name 'php-cgi*' -o -name 'libphp*.so' -o -name 'lsphp' \) 2>/dev/null

On Debian and Ubuntu, list every installed PHP package, then confirm the fix through the changelog rather than the version string:

dpkg -l 'php*' 'libapache2-mod-php*' | awk '/^ii/ {print $2, $3}'
apt-cache policy php8.4-fpm
apt changelog php8.4-fpm 2>/dev/null | grep -m1 -E 'CVE-2026-10961[89]'
sudo apt update && sudo apt install --only-upgrade 'php8.4*'

The Hacker News notes that distro packages can carry the fix under an older-looking version number, so a php -v that does not say 8.3.36 is not proof of exposure on a distro build. The CVE in the package changelog is the real test.

On RHEL, AlmaLinux, Rocky and Fedora, the --cve filter is documented for updateinfo and upgrade in the DNF command reference:

rpm -qa 'php*' | sort
dnf module list php                     # which php stream is enabled (RHEL 8 and 9)
dnf updateinfo list --cve=CVE-2026-109618
sudo dnf upgrade --cve=CVE-2026-109618 --cve=CVE-2026-109619
rpm -q --changelog php-fpm | grep -m1 -E 'CVE-2026-10961[89]'

If the advisory has not landed for your stream yet, the commands return nothing. That is a "wait and isolate" result, not a clean bill of health. On Windows servers, run php.exe -v from each PHP directory IIS or your application points at, and replace the build with the 8.x release from the php.net downloads page.

2. Containers

The official php image on Docker Hub compiles PHP from the php.net source tarball (we checked the 8.3 bookworm FPM Dockerfile, which sets ENV PHP_VERSION 8.3.36). It also blocks Debian's own PHP packages. So an image built FROM php:... gets the fix only by pulling a new base and rebuilding. Running apt upgrade inside it will not touch PHP.

# What PHP does each local image actually ship?
for img in $(docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  printf '%s  ' "$img"
  docker run --rm --entrypoint php "$img" -r 'echo PHP_VERSION, PHP_EOL;' 2>/dev/null || echo "no php binary"
done

# Which images are running in Kubernetes right now (from the Kubernetes docs)
kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec['initContainers', 'containers'][*].image}" |\
tr -s '[[:space:]]' '\n' | sort | uniq -c

# Rebuild on a fixed base, ignoring stale cache
docker build --pull --no-cache -t myapp:php-8.3.36 .

A few image facts worth knowing, all from the docker-library sources:

  • Supported tags follow php:<version>-<variant>-<suite>, with variants cli, apache, fpm and zts, on Debian trixie and bookworm and on Alpine 3.24 and 3.23.
  • The official image definitions list 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The Docker Hub description page lagged behind when we checked (it still showed 8.5.11 and 8.3.35), so trust docker run ... php -v over the web page.
  • There are no maintained 8.1, 8.0 or 7.4 tags. An image built FROM php:8.1-fpm will never receive this fix from Docker Hub.
  • If your Dockerfile pins a digest, the floating tag moving does nothing for you. Update the digest.

Images that install PHP from distro packages (for example FROM debian:bookworm plus apt-get install php8.2-fpm, or Alpine plus apk add) need the distro fix and a rebuild. Check those with docker run --rm IMAGE dpkg -l 'php*' or apk list --installed | grep -i php. FrankenPHP's standalone Linux binaries are statically linked, per its docs, so the PHP inside changes only when you install a newer FrankenPHP build or image.

3. Shared hosting and managed platforms

If someone else runs the server, they own the PHP upgrade. Huntress specifically calls out shared hosting and control panels as large, lightly watched PHP estates. Ask your provider for the package version and the CVE, in writing.

If you run the panel yourself:

  • cPanel and WHM. EasyApache 4 installs each PHP as an ea-phpNN package with a CLI link at /usr/local/bin/ea-phpNN and a config at /opt/cpanel/ea-phpNN/root/etc/php.ini, according to cPanel's docs. Run ls /usr/local/bin/ea-php* and then /usr/local/bin/ea-php83 -v for each one. cPanel delivers these through OS package updates, so dnf upgrade 'ea-php*' or the WHM update preferences apply. cPanel says packages for unsupported versions stay on its mirrors but get no further updates.
  • Plesk. Tools and Settings, then PHP Settings, shows every PHP version and handler and how many domains use each. Patch the versions in use, then retire unused ones.
  • Managed PaaS. Check the platform's status page and runtime release notes, then redeploy so your app lands on the patched runtime.

4. Appliances and third-party software

PHP sits inside products that never mention it. Huntress lists vendor appliances and Windows applications that ship php.exe or php-cgi.exe. Build a short list of anything with a web admin panel (NAS boxes, monitoring tools, ticketing systems, VPN and firewall consoles, help desks) and check each vendor's advisory page. Do not patch an appliance's PHP by hand unless the vendor supports it; you can break the product and void support. Until a vendor fix exists, keep the admin interface off the internet.

5. End-of-life 7.x, 8.0 and 8.1

PHP 8.1 and older get no new releases from the PHP project. You have three honest options:

  1. Use a distro or LTS vendor package that backports the fix, and confirm the CVE in its changelog. cPanel's docs point users who need patched old versions to TuxCare's Extended Lifecycle Support.
  2. Build from source using the security team's minimal backport branches for 7.4, 8.0 and 8.1, linked from the php.net advisory. Only do this if you already build PHP yourself.
  3. Upgrade to 8.2.35 or later. Note that 8.2 itself reaches end of life on 31 December 2026, so 8.3 or newer is the better target.

6. Restart so the new binary is actually loaded

A package upgrade replaces files. The running PHP-FPM master and the Apache parent with mod_php keep the old code until they restart. Use a full restart, not a reload, and restart every pool.

# Debian / Ubuntu
sudo systemctl restart php8.4-fpm
sudo systemctl restart apache2

# RHEL family
sudo systemctl restart php-fpm
sudo systemctl restart httpd

# LiteSpeed / OpenLiteSpeed: restart the web server so lsphp workers respawn

Also restart long-running PHP workers that are not web servers: queue workers, Laravel Octane, RoadRunner, Swoole and supervisor-managed daemons. In containers, a rebuilt image only helps after the pods or containers are replaced, for example with kubectl rollout restart deployment/<name>.

7. Verify after deploy

Verification has to look at the running processes, not the disk.

# Debian / Ubuntu: which services still use replaced files (list only, no restarts)
sudo needrestart -r l

# RHEL family (dnf-plugins-core): systemd services needing a restart
sudo dnf needs-restarting -s

# Any PHP or Apache process still mapping a deleted (replaced) file
for p in $(pgrep -f 'php-fpm|apache2|httpd|lsphp'); do
  grep -q '(deleted)' /proc/$p/maps 2>/dev/null && echo "PID $p still maps a replaced file"
done

# When did the FPM master start, and when was the package installed?
ps -o pid,lstart,cmd -p "$(pgrep -o -f 'php-fpm: master')"
grep -E ' (upgrade|install) php' /var/log/dpkg.log | tail -5     # Debian / Ubuntu
rpm -q --last php-fpm                                          # RHEL family

If the master started before the package install, it is still running old code. For containers, kubectl get pods -o wide plus the image digest on each pod tells you whether the replacement finished. Finally, re-run your vulnerability scanner against the live estate and keep the output as evidence.

8. WAF rules are a stopgap only

Tenable describes Cloudflare's emergency WAF rules as a stopgap that covers only traffic routed through Cloudflare, and the same logic applies to any WAF. A request that reaches the origin another way, through a direct IP, an internal network, or a second CDN, bypasses it. The php.net mitigations (memory_limit=-1, a low post_max_size, session.upload_progress.enabled=0, and on 8.5 fatal_error_backtraces=0) narrow specific paths, but the advisory is clear that they do not make an installation safe. If you cannot patch a system soon, the UK NCSC's advice, as reported by Help Net Security, is to consider taking it offline. Our post on governments taking websites offline over PHP2Shell covers how public bodies applied that.

What would have helped

Practices any team can use

  • A runtime inventory, not just an app inventory. Most teams can list their applications. Fewer can list which PHP binary, SAPI and version sits under each one. A table of host or image, PHP version, SAPI and owner turns a crisis into a checklist.
  • One patch path per runtime type. Distro packages, official images, panel packages, static binaries and vendor appliances each update differently. Writing down the path for each ahead of time is what lets you move in hours.
  • Rebuild and redeploy on a schedule. Images rebuilt weekly with --pull pick up base fixes without heroics. Images that have not been rebuilt in a year are where old PHP hides.
  • Restart as part of the patch. Make the restart and the needrestart or needs-restarting check part of the same runbook step as the upgrade.
  • Stay on supported branches. Every 7.x, 8.0 and 8.1 system turned this into a vendor hunt. Plan the move off 8.2 before 31 December 2026.
  • Know your appliances. A vendor inventory that records which products expose a web console makes the third-party check a ten-minute job.
  • Process monitoring on web workers. Huntress suggests alerting when php-fpm, php-cgi, httpd or apache2 spawn shells or download tools. That helps you catch what patching cannot undo.

For a broader method, see our guide to SBOM-driven incident response and our earlier write-up on PHP security.

Where Safeguard fits

Safeguard helps with the container and repository side of this. It does not patch hosts or restart processes for you.

  • See which PHP an image ships. safeguard scan --image <name> builds an SBOM of the image and lists the PHP build inside it, which you can compare with the fixed versions. Results land as JSON under ./output. You can also scan a repo with safeguard scan --dir . or an existing SBOM file with safeguard scan --sbom <file>. Vulnerability matching depends on the databases carrying these new CVEs, so check the version itself for now.
  • Find every project with the affected component. The platform shows which projects contain a given component and version, which answers "where else do we ship PHP 8.1" without grepping Dockerfiles.
  • Gate the next build. Once the CVEs appear in your findings, policies and gates evaluated in the platform can stop an image that still carries them from being promoted.
  • Make the change with approval. The Safeguard CLI's assistant can edit a Dockerfile base tag and run the rebuild with your approval, under the /permissions mode you choose. safeguard exec runs a single non-interactive assistant turn, which suits CI. Autonomous remediation can open fix PRs, but it is off by default.

Where it does not help: it cannot see PHP on a shared host or appliance you do not control, a statically linked PHP may not appear as a package at all, and no scan tells you whether a running FPM master restarted. Those still need the host checks above. To try it, download Safeguard and follow the CLI docs.

Common mistakes

  • Trusting php -v alone. The CLI and the FPM or Apache module are separate packages on most distros. Check every SAPI package.
  • Reading version strings on distro builds. A backported fix keeps the old upstream number. Look for the CVE in the changelog.
  • Running apt upgrade inside an official php image. PHP there is compiled from source; only a new base image changes it.
  • Forgetting digests and old tags. Pinned digests and php:8.1-* tags never move.
  • Reloading instead of restarting. Restart every pool and the Apache parent, then verify with needrestart or dnf needs-restarting.
  • Updating WordPress and calling it done. The bug is in the runtime, not the CMS.
  • Treating the WAF as the fix. It covers only traffic through it, and only known vectors.

FAQ

Which PHP versions fix PHP2Shell?

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, released on 21 October 2026, or a distro package whose changelog lists CVE-2026-109618 and CVE-2026-109619.

Is the PHP CLI affected?

The php.net advisory scopes the issue to installations that serve web requests and does not say whether the CLI alone is affected. Patch it anyway, since it usually comes from the same source package.

My distro still shows PHP 8.2.x below 8.2.35. Am I vulnerable?

Maybe not. Distros backport fixes without changing the upstream version. Search the package changelog for the CVE IDs. If they are absent, you are not patched yet; Debian's tracker listed bookworm's php8.2 as vulnerable when we checked.

How do I patch a container built on the official php image?

Pull the fixed tag (for example php:8.3.36-fpm-bookworm), rebuild with docker build --pull, push, and replace the running containers. If you pin a digest, update it.

What about PHP 7.4, 8.0 and 8.1?

There is no upstream release. Use a distro or LTS vendor backport, build from the php.net backport branches, or upgrade. watchTowr's FAQ and php.net differ on whether PHP 7 has any patch, so check the php.net advisory directly.

Do I need to restart after updating?

Yes. Restart PHP-FPM, Apache, and any long-running PHP workers. The Hacker News and the php.net advisory both say every worker must restart.

Is a WAF rule enough?

No. It is a stopgap for known vectors on traffic that passes through it. Patch, and if you cannot, isolate the system.

Try it yourself

Watch a real Safeguard CLI session that finds a vulnerable dependency, asks before each command, makes the fix and pushes nothing.

Click through it at your own pace, or open it full screen.

Set up the runner on your own machine, approve it with its device code, then scan from the terminal.

Click through it at your own pace, or open it full screen.

Quick check

You upgraded php8.4-fpm and php -v shows the fix. Is the site safe?

Not yet. The running PHP-FPM master still holds the old code until you restart it. Restart every pool, then confirm with needrestart -r l or dnf needs-restarting -s.

Your Dockerfile says FROM php:8.1-apache. How do you get the PHP2Shell fix?

You cannot get it from that tag, because the official image definitions no longer include 8.1. Move to a fixed supported tag such as php:8.3.36-apache, rebuild with --pull, and redeploy.

Sources

Last reviewed: 21 October 2026. This is a developing story; check the linked advisories for the latest status.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.