Updated 24 October 2026: after review, I corrected Lesson 3 on how to check that a server is patched.

PHP2Shell is two critical bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, fixed upstream in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. In short: check that your PHP package has the fix, then restart every PHP worker. This week I spent a lot of time on this problem. I checked servers, patched PHP, and answered the same questions again and again. Some of what I believed on Tuesday was wrong by Friday. Here is what I learned.

What PHP2Shell is

PHP2Shell is the name for two critical bugs in PHP itself: CVE-2026-109618 and CVE-2026-109619. If PHP is not patched, an attacker can run code on the server without logging in.

The details leaked on 20 October 2026. php.net shipped the fixes on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Unpatched builds of older versions are affected. But Linux distributions also ship the fix in packages that keep an older version number, so the version number alone does not tell you if a server is exposed.

Lesson 1: updating the framework does not help

The first question I heard was “We updated WordPress. Are we safe?” The answer is no.

The bugs sit in the code PHP runs while it sets up a request. That happens before WordPress, Laravel, your middleware or your login check runs. So a plugin update, a security plugin or composer update laravel/framework changes nothing. Your app runs on top of PHP. It does not replace PHP.

This also means your usual tools stay quiet. composer audit and GitHub Dependabot check the packages in composer.lock. They do not check the PHP binary on the server. A clean composer audit report tells you nothing about PHP2Shell. You have to check the PHP version on the server, and then restart every PHP process: PHP-FPM, Apache, queue workers and long-running workers such as Octane. Workers keep the old code in memory until they restart.

Lesson 2: a WAF helps only if nobody can go around it

Cloudflare shipped emergency WAF rules on 21 October 2026, on all plans, including Free. On Free plans they turn on by themselves. On paid plans, check that the managed rules are in Block mode. That is good. But Cloudflare calls these rules a stopgap. A new variant of the attack can pass until the next rule update.

The bigger problem is simpler. A WAF only checks the traffic that passes through it. If your origin server also answers requests sent straight to its IP address, an attacker can skip the WAF completely.

I found this gap on more than one server this week. It is easy to miss. You test the site through its domain name, the WAF blocks the test, and everything looks protected. The fix is to lock the origin firewall so it accepts web traffic only from the WAF’s IP ranges. Then patch PHP anyway.

Lesson 3: verify the running PHP service, not just php -v

The standard advice is “run php -v and check the version”. There are two traps here.

Trap 1: the version number can be old even when the fix is installed. Distribution packages often backport the fix and keep the old version number. On Ubuntu 24.04, the patched php8.3 package is 8.3.6-0ubuntu0.24.04.13, so php -v still shows 8.3.6. On Ubuntu 22.04, the patched php8.1 package is 8.1.2-1ubuntu2.28. Both are listed in USN-8948-1. The same is true for the backported PHP 8.1, 8.0 and 7.4 builds in Lesson 4. So “below 8.3.36” is not proof that a server is exposed. Compare the package revision with the advisory for your operating system and package source.

On a Debian or Ubuntu server, the package changelog also shows if the fix is in the package:

zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-cli/changelog.Debian.gz

Change 8.3 to match your version. Note that this pattern matches either CVE id, so check that both ids appear. A match shows that the installed package has the fix. No match means this check found nothing, not that the server is exposed, so compare the package revision with the advisory instead.

Trap 2: a patched package does not prove the running workers restarted. On Debian and Ubuntu, the package upgrade restarts php-fpm by itself. On current releases, the Apache mod_php package also reloads Apache. But long-running CLI workers do not restart: queue workers, Horizon, Octane and Supervisor jobs keep running the old PHP until you restart them by hand. Restart them after every PHP upgrade.

Lesson 4: “PHP 8.1 and older has no fix” turned out to be wrong

On 21 October 2026, the message from almost everyone was clear: PHP 8.1 and older are end of life, so no fix will come. Plan an upgrade, and isolate the server until then. I repeated that message too.

It was only half true. The PHP project did not release new versions of 8.1, 8.0 or 7.4. But the PHP security team did publish small source patches for those three versions. Package maintainers used them:

  • Ondřej Surý’s PHP packages (ppa:ondrej/php) shipped fixed builds on 21 October 2026 for Ubuntu 22.04 and 24.04: 8.1.34-9, 1:8.0.30-25 and 1:7.4.33-31.
  • Laravel Forge installs PHP from those packages. On 23 October 2026, Forge told customers by email that patched versions are now available for every PHP version on Forge, from 5.6 to 8.5. You click “Update PHP” on each server. The sources conflict here: the Forge knowledge-base page still says PHP 8.1 and earlier are not patched.
  • Ubuntu fixed its own php8.1 package on 22.04 (8.1.2-1ubuntu2.28, USN-8948-1).
  • Remi reports fixed php81, php80 and php74 builds for RHEL, Alma and Rocky.

There are limits. These builds come from package maintainers, not from the PHP project. The PPA has no build for Ubuntu 20.04. Ubuntu Pro has a fix for the Ubuntu php7.4 package on 20.04 (USN-8948-2); with Pro, run pro fix CVE-2026-109618 and pro fix CVE-2026-109619. Without Pro, move the sites to a newer server. ARM servers got their builds later than x86 servers. And the official Docker php:8.1 image has no fix.

A patched PHP 8.1 is a short-term fix, not a plan. Upgrade anyway. Aim for PHP 8.3 or later, because PHP 8.2 security support ends on 31 December 2026.

The lesson for me: when a vendor says “no fix exists”, check again two days later. The situation moved fast this week.

Lesson 5: treat Docker images built before 21 October as unpatched

Containers hide the PHP version from you. The host can be fully patched while the container still runs old PHP. Check inside the running container:

docker exec <container> php -v

An image built before 21 October 2026 is almost certainly unpatched. Do not trust a floating tag such as 8.4-fpm either. Pin a tag that names the fixed version, such as php:8.4.27-fpm, and rebuild with docker compose build --pull so Docker downloads the new base image. The build does not replace the running containers. Run docker compose up -d to recreate them, or do both in one step with docker compose up -d --build --pull always. For services that use a ready-made image: without build:, run docker compose pull first. Then check the version inside the container again.

The full step-by-step guide

This post covers what surprised me. For the full checklist, the commands for each platform, the temporary measures if you cannot patch yet, and the exploitation reports, read the AtasAI guide: PHP2Shell explained: who is affected, how to patch, and why updating WordPress is not enough.

Want someone to check your PHP setup? AtasAI offers a free PHP patch-status audit. Email me at aidan@atas.ai