· 12 min read

Illustration: two red dragon-shaped attacks strike a server bearing the PHP logo. One hits a Cloudflare shield and the other goes around it and reaches the server, which feeds several WordPress sites.

Short version: on October 21, 2026, PHP released 8.5.12, 8.4.27, 8.3.36 and 8.2.35, which fix two critical flaws (CVE-2026-109618 and CVE-2026-109619, CVSS 9.2 each) now known as "php2shell". They let someone with no account and no password run code on your server, and in the worst case it happens before your application even loads, so no security plugin sees it. Updating WordPress or your plugins does nothing here: you have to update PHP and restart the service. Cloudflare already blocks the attack, even on the free plan, but only for traffic that goes through Cloudflare. Below are the commands to check a VPS, Docker containers and shared hosting.

What happened and why everyone is talking about it

On October 20 the PHP security team did something unusual: it announced a day in advance that a fix would ship on Wednesday the 21st at 14:00 UTC, so everyone could get ready. The notice said there were two critical vulnerabilities and that the release could come earlier if attacks showed up in the wild.

The warning spread quickly among hosting providers and developers, many of whom emailed their customers to have an update plan ready. After the fix shipped, the CERT for the EU institutions published advisory 2026-018, recommending updating "as soon as possible" or isolating from the internet anything that could not be updated.

The fix came out on the 21st with this advisory: "PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical vulnerabilities that can allow unauthenticated remote code execution." Unauthenticated remote code execution is the worst category there is for a web server.

The two flaws, without the jargon

You don't need to understand PHP's memory internals to protect yourself, but it helps to understand why the fix is what it is.

CVE-2026-109618: corruption when memory runs out. When PHP runs out of memory while it is starting to process a request, it bails out immediately. If that happens at the wrong moment, it leaves internal data half set up, and the cleanup step then treats that data as valid. An attacker who sends a large request designed to trigger that cutoff can abuse the cleanup to run code. According to the GitHub advisory, the attack is proven on Apache with mod_php, and based on the source code FPM, FrankenPHP and LiteSpeed also look vulnerable.

CVE-2026-109619: leftovers from a previous request. A single PHP process handles many requests in a row. This flaw lets some data from one request survive after its memory has been freed, and the next request can use it before it gets reset. According to its advisory, it is proven on mod_php and validated on PHP-FPM through the session upload progress feature (session.upload_progress), which is on by default.

What makes php2shell serious is this: on mod_php, the attack happens before a single line of your application runs. Wordfence, Solid Security and every other security plugin live inside WordPress, so they never see the request. The same goes for Nextcloud, Laravel or anything else written in PHP.

Who is affected

Anyone serving pages with PHP, in any of its forms:

How PHP runsRisk according to PHP
Apache with mod_phpHighest. Working exploit shown for both flaws
PHP-FPM (nginx, Caddy, Apache as a proxy)Affected. Validated for the second flaw
FrankenPHP, LiteSpeed (LSAPI), php-cgiAffected according to the official advisory

Versions:

BranchFixed inNote
8.58.5.12
8.48.4.27
8.38.3.36
8.28.2.35
8.1 and olderNo official fixPHP no longer maintains them. Only your distro or your provider can save you

Cloudflare, in its October 21 post, says "PHP 5.4 or later". That does not contradict PHP: older versions have the flaw too, the PHP project just won't fix them. If you have a forgotten PHP 7.4 somewhere, you are not out of this. You are worse off.

And this is not theoretical. Cloudflare says it has already seen attack traffic against its customers, with attackers changing the payload to try to get past the rules. CloudLinux mentions an exploit that is already circulating publicly.

Step 1: find out which PHP actually serves your site

The most common mistake is running php -v, seeing a number and moving on. That command shows the command-line version, which is not necessarily the one serving your website. A server can have several versions installed at once.

On Debian or Ubuntu, first see which versions are installed and which ones are running as a service:

ls /etc/php/
systemctl list-units --type=service 'php*-fpm*'

Then ask the FPM version that is actually running (replace 8.3 with yours):

php-fpm8.3 -v

If you use Apache with mod_php, this tells you whether it is loaded:

apache2ctl -M | grep -i php

If you don't have shell access, in WordPress go to Tools → Site Health → Info → Server, which shows the PHP version the site is running right now.

Watch out for distro version numbers

If you use the official Ubuntu or Debian packages, the number will not match the table above, and that does not mean you are exposed. Distros apply the fix on top of their original version without changing the main number. For example, Ubuntu 24.04 ships PHP 8.3.6, and the fixed version is 8.3.6-0ubuntu0.24.04.13. What matters is the package suffix, not the 8.3.6.

Fixed versions in the official repositories, checked against USN-8948-1 and the Debian security tracker:

SystemPackageFixed version
Ubuntu 26.10php8.58.5.9-0ubuntu2.1
Ubuntu 26.04 LTSphp8.58.5.4-0ubuntu1.5
Ubuntu 24.04 LTSphp8.38.3.6-0ubuntu0.24.04.13
Ubuntu 22.04 LTSphp8.18.1.2-1ubuntu2.28
Debian 13 (trixie)php8.48.4.27-1~deb13u1 (DSA-6574-1)
Debian 12 (bookworm)php8.28.2.35-1~deb12u1 (DLA-4848-1)

If you use Ondřej Surý's repository (ppa:ondrej/php or packages.sury.org), which is the usual choice when you need a newer PHP than your distro ships, you will see the upstream numbers: 8.3.36, 8.4.27 and so on.

To see the exact package you have installed:

apt list --installed 'php8.3-fpm'

Step 2: update and restart

On Debian or Ubuntu:

sudo apt update
sudo apt upgrade

And here is the part people forget. Updating the package changes nothing until you restart PHP. The FPM or Apache processes that were already running keep using the old code they have loaded in memory. The second flaw lives precisely in those long-running processes.

sudo systemctl restart php8.3-fpm      # if you use FPM (change the version)
sudo systemctl restart apache2         # if you use mod_php

To confirm the restart happened after the update, compare these two times:

grep 'upgrade php8.3-fpm' /var/log/dpkg.log | tail -1
systemctl show php8.3-fpm -p ActiveEnterTimestamp

Here is an example from a server that was updated correctly: the package was installed at 20:22:14 and the service started at 20:22:36. If on yours the service time is earlier than the update time, you are still running vulnerable PHP even though apt says everything is up to date.

2026-10-21 20:22:14 upgrade php8.3-fpm:amd64 8.3.35-1+ubuntu24.04.1+deb.sury.org+1 8.3.36-1+0~20261021.88+ubuntu24.04~1.gbp250f7c
ActiveEnterTimestamp=Wed 2026-10-21 20:22:36 UTC

On RHEL, Rocky or Alma, the equivalent is sudo dnf upgrade 'php*' followed by the same restart of php-fpm or httpd.

Step 3 (homelab): check your containers

If you run a homelab, there is a good chance the host's PHP is up to date and the problem is inside a container. Every Docker image ships its own PHP, and apt upgrade on the host doesn't touch it. Nextcloud, WordPress, phpMyAdmin, FreshRSS, Matomo, Firefly III: if the app is written in PHP, its container has PHP inside.

To check every running container at once:

for c in $(docker ps -q); do
  echo "$(docker inspect -f '{{.Name}}' $c): $(docker exec $c sh -c 'php -v 2>/dev/null | head -1')"
done

Containers without PHP show an empty version. For the rest, check the number.

The good news is that the official images were rebuilt on October 21 itself. I checked Docker Hub: php:8.3.36-fpm, php:8.4.27-fpm, php:8.5.12-fpm and php:8.2.35-fpm already exist, and wordpress:latest and nextcloud:latest were updated that same afternoon. To pull the new image and recreate the container:

docker compose pull
docker compose up -d

Two details:

Step 4: if you are on shared hosting

You can't update PHP yourself here, so what you can do is ask. Open a ticket or a chat and ask, in these words if you like: "Have you applied the PHP patches for CVE-2026-109618 and CVE-2026-109619 on the server where my account lives, and restarted the PHP processes?" If they can't answer clearly or don't have a date, that is a good sign to start looking at other providers.

A useful detail: most shared hosting runs cPanel with CloudLinux. According to CloudLinux, the fixed ea-php packages (the cPanel ones) are already out for every version, from 5.2 to 8.5. The alt-php packages depend on the CloudLinux version your provider runs. In other words, your provider has no technical excuse for not having updated already.

Because a provider's patched build doesn't always change the version number, checking the version in your control panel isn't enough to confirm it. The support team's answer is what counts.

If you can't update today

It happens: an old application that won't run on the new version, a server you don't manage, a maintenance window that hasn't arrived yet. PHP's official advisories suggest partial mitigations. None of them replaces the fix, and PHP itself says so: updating is "the only complete mitigation".

For the second flaw (CVE-2026-109619), turn off session upload progress. In php.ini:

session.upload_progress.enabled = 0

Or in the FPM pool (for example /etc/php/8.3/fpm/pool.d/www.conf), which has the advantage that the application can't turn it back on:

php_admin_flag[session.upload_progress.enabled] = off

Almost no application uses that feature. If you run PHP 8.5, the advisory also recommends fatal_error_backtraces=0. In both cases you need to restart FPM or Apache for the change to apply.

For the first flaw (CVE-2026-109618), the main mitigation PHP suggests is memory_limit=-1, which removes the memory limit so the error that triggers the flaw never fires. It has an obvious cost: a badly written script or an attack can eat all the server's memory. If you do it, set a limit from outside, for example the container's memory limit or a systemd one. PHP also recommends keeping post_max_size low and rejecting large requests before they reach PHP. In nginx, that is client_max_body_size.

If you run FPM behind nginx, make sure your config does not pass the PHP_VALUE or PHP_ADMIN_VALUE parameters to PHP. The advisory lists that as one of the conditions that expose FPM to the second flaw:

grep -rn -E 'PHP_VALUE|PHP_ADMIN_VALUE' /etc/nginx/

If it returns nothing, you are not passing them.

Cloudflare: one more layer, with a clear limit

Cloudflare shipped rules for php2shell on October 21 at 03:02 UTC, about nine hours before the details went public. The interesting parts:

To check, open your zone, go to Security → WAF → Managed rules and confirm the ruleset is active and that you have no exception turning it off.

The limit is this: the WAF only protects traffic that goes through Cloudflare. If your DNS record has the gray cloud (DNS only) instead of the orange one (proxied), Cloudflare sees nothing. And if your server has port 80 or 443 open to the internet, an attacker who knows the real IP can skip Cloudflare and go straight to it. There are public tools dedicated to finding those IPs.

For a homelab, Cloudflare Tunnel solves this at the root: your server opens no ports and all traffic comes in through the tunnel, so there is no way to reach the origin without going through the WAF. If you have never set one up, I walk through it in my ActivePieces with Docker and Cloudflare Tunnels guide. If you prefer open ports, restrict access in your firewall to Cloudflare's IP ranges.

Even so, Cloudflare says it clearly in its post: the WAF is an extra layer of defense, and you still have to patch.

After patching: check whether someone got in first

The fix closes the door, but it doesn't kick out anyone who already got in. If your server was exposed between October 21 and the moment you updated, a quick review is worth it. On WordPress:

# PHP files inside uploads: there should only be images and documents there
find wp-content/uploads -name '*.php'

# Administrator accounts: do you recognize all of them?
wp user list --role=administrator

# PHP files modified in the last 3 days
find . -name '*.php' -mtime -3

Outside WordPress, also check cron jobs (crontab -l for each user and /etc/cron.d/) and any outbound connections you don't expect. If you find something odd, don't just delete it: the safest move is to restore from a backup taken before October 21 and rotate every password and key. A password manager like self-hosted Passbolt helps you not miss any.

Quick checklist

  1. Find out which PHP version actually serves your site (FPM or Apache, not just php -v).
  2. Update to a fixed version, or to your distro's fixed package.
  3. Restart PHP-FPM or Apache, and confirm the restart happened after the update.
  4. Check your Docker containers one by one.
  5. On shared hosting, ask support in writing.
  6. If you can't update today, apply the mitigations and put a date on the fix.
  7. If you use Cloudflare, confirm the managed rules are active and that your origin can't be reached directly.
  8. Look for signs that someone got in before you patched.

If you have a site on PHP 8.1 or older, this is a good time to plan the migration. This time your provider or your distro covered you. Next time, they might not.

Sources

Leave a Reply

Your email address will not be published. Required fields are marked *