Guides+45 XP

php2shell: the two critical PHP vulnerabilities and what to do on your servers today

CVE-2026-109618 and CVE-2026-109619 allow unauthenticated code execution. How to tell if you're affected, patch properly, mitigate if you can't, and watch.

IvánPublished on 8 minLeer en español

On October 20, two critical PHP flaws leaked ahead of schedule, and today, October 21, the PHP team shipped emergency patches. They’re called php2shell (CVE-2026-109618 and CVE-2026-109619) and the name says it all: a single request, with no username or password, can end up running commands on your server before a single line of your application executes. Some European government bodies have taken sites offline as a precaution, and Cloudflare and watchTowr are already seeing exploitation attempts. Here’s what’s going on, how to tell if it hits you and what to do today, in order.

This is a generic guide written on 2026-10-21 from the PHP, CERT-EU and INCIBE advisories plus the Huntress and Tenable write-ups. At home I don’t have anything running PHP exposed to the internet, so this isn’t a story of a specific patch run but how I’d go about it. If you’re reading this days later, check the official advisory first for changes.

1. What’s going on

These are two memory bugs in PHP itself, not in WordPress or any framework. Both are rated CVSS 9.2 and lead to remote code execution as the web server user (www-data, apache…):

CVE What breaks, in plain terms
CVE-2026-109618 PHP runs out of memory (memory_limit) halfway through parsing a request, for example a file upload. During cleanup it uses data it never finished setting up.
CVE-2026-109619 After a fatal error, PHP keeps pointers to memory that has already been freed. The next request handled by that same process may use them.

Any PHP that serves web pages is affected: PHP-FPM (the usual setup with Nginx), Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The PHP team considers mod_php the most dangerous case, because there’s a demonstrated exploit that works with the default configuration.

And PHP lives in far more places than it seems: WordPress, Drupal, Joomla, Moodle, Nextcloud, GLPI, panels like cPanel or Plesk, and plenty of commercial apps that bundle it without telling you.

2. Am I affected?

Fixed versions:

Branch Fixed in
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35
7.4, 8.0, 8.1 No official release; source patches only, for people who build PHP
7.3 and older No fix. The bug goes back to PHP 5.4

The hard part isn’t the table, it’s finding every PHP you own. I’d start with this on each Linux server:

# Installed packages (Debian/Ubuntu and RHEL/Rocky)
dpkg -l | grep -E '^ii +(php|libapache2-mod-php)'
rpm -qa | grep -i php

# Which PHP is actually running right now
ps -eo pid,lstart,cmd | grep -E 'php-fpm|php-cgi|apache2|httpd' | grep -v grep

# FPM and Apache module versions (don't rely on `php -v` alone)
php-fpm8.3 -v 2>/dev/null; apache2ctl -M 2>/dev/null | grep -i php

php -v tells you the console version, which is a separate package. You can have php8.3-cli up to date while php8.2-fpm serves the site on the old version.

If you use containers, each image ships its own PHP:

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

And don’t forget the things that aren’t “PHP servers”: Windows apps that bundle php-cgi.exe, appliances, the shared hosting for your local club’s website. The Dutch NCSC keeps a list of affected third-party software on GitHub (NCSC-NL/php2shell). For anything that’s published, the ss -ltnp guide helps you see what’s listening on each machine.

3. Update (and restart, which is where people slip)

Distros got the patch before it went public, so normally you just update with your package manager:

# Debian / Ubuntu
sudo apt update
sudo apt install --only-upgrade 'php*' 'libapache2-mod-php*'

# RHEL / Rocky / Alma
sudo dnf upgrade 'php*'

Watch the version number. Debian, Ubuntu and RHEL don’t jump to 8.2.35: they backport the fix into their usual version and only change the package suffix. What you need to check is that the changelog mentions the CVEs:

# Debian / Ubuntu
apt changelog php8.2-common 2>/dev/null | grep -E 'CVE-2026-10961[89]'

# RHEL and derivatives
rpm -q --changelog php-common | grep -E 'CVE-2026-10961[89]'

If nothing shows up, your distro hasn’t shipped it yet: go to step 4 in the meantime.

Then restart everything that runs PHP. An updated package is useless if the old processes are still in memory:

sudo systemctl restart php8.2-fpm      # one per FPM version you have
sudo systemctl restart apache2         # if you use mod_php (httpd on RHEL)

# Check the processes are new: the start time should be after the patch
ps -eo pid,lstart,cmd | grep -E 'php-fpm|apache2|httpd' | grep -v grep

With Docker, the official php:* images have already been rebuilt. If your image builds on one of them (FROM php:8.3-fpm), you need to rebuild it and force a fresh base pull; restarting the container isn’t enough:

docker compose build --pull
docker compose up -d

For third-party images (WordPress, Nextcloud…), run docker compose pull && docker compose up -d and use the loop from step 2 to check the version changed. If you’re not sure how that’s laid out, it’s in the Docker Compose guide.

If you use the Remi repository (rpms.remirepo.net), it won’t have new packages today. The PHP team recommends switching to your distro’s packages or building from source.

4. If you can’t update today

The PHP team is blunt about it: only the patch fixes this. The steps below close the known attack paths and buy you time, but they don’t make the server safe.

In a PHP-FPM pool (for example /etc/php/8.2/fpm/pool.d/www.conf):

; CVE-2026-109619: closes the confirmed path through session.upload_progress
php_admin_flag[session.upload_progress.enabled] = off

; CVE-2026-109618: removes the memory limit that triggers the bug and caps request size
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M         ; 32M if the app needs file uploads
php_admin_value[upload_max_filesize] = 6M   ; always below post_max_size

; PHP 8.5 only
; php_admin_flag[fatal_error_backtraces] = off

With Apache and mod_php, the same in the VirtualHost or a global .conf:

php_admin_flag  session.upload_progress.enabled Off
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 6M

I use php_admin_* on purpose: the application can’t change it back. Also make sure no other pool, .htaccess or directory sets a finite memory_limit again.

Removing the memory limit has a cost: a runaway script can eat the server’s RAM. The advisory itself suggests enforcing the cap outside PHP, with the service’s cgroup. With systemd:

sudo systemctl edit php8.2-fpm
[Service]
MemoryMax=2G
OOMPolicy=continue

OOMPolicy=continue means that if the limit is hit, the kernel kills the hungriest process and the service stays up. Don’t use ulimit -v or RLIMIT_AS: they cause exactly the memory failure you’re trying to avoid.

Then sudo systemctl restart php8.2-fpm (or Apache) so it applies to every worker.

Two more layers that help:

  • A WAF in front. Cloudflare has published rules for the known vectors, including on the free plan. It only protects traffic that goes through Cloudflare, and they call it “a stopgap” themselves.
  • Isolate what can’t be fixed. A PHP 7.x or older exposed to the internet today is an open door. Put it behind a VPN or switch it off until it’s migrated: that’s CERT-EU’s advice and what some government bodies have done.

5. Check whether someone got in

If the server has been exposed since yesterday, patching isn’t enough: you need to look. Almost every PHP intrusion gives itself away the same way: the web process launching things it shouldn’t (sh, bash, curl, wget, python, nc).

With auditd you can log every program the web user runs (on Debian www-data is UID 33):

sudo auditctl -a always,exit -F arch=b64 -S execve -F uid=33 -k php_exec
sudo ausearch -k php_exec -i | grep -E 'comm=(sh|bash|dash|curl|wget|nc|python3?|perl)'

If you run Wazuh, that auditd rule already reaches the console and you can alert on it. I went into that in more detail in the Wazuh and GLPI post.

Three more quick checks:

# PHP files created or modified in the web root since the leak
sudo find /var/www -name '*.php' -newermt '2026-10-20' -ls

# Workers dying from memory faults: exploitation attempts often leave this trail
sudo grep -iE 'signal 11|SIGSEGV|Segmentation fault' /var/log/php*-fpm.log /var/log/apache2/error.log

# Scheduled tasks and new users
sudo ls -la /etc/cron.* /var/spool/cron/crontabs; sudo tail -5 /etc/passwd

A spike of signal 11 doesn’t prove an attack, but this week it’s worth a look. If you find something odd, isolate the server and save the logs before touching anything.

What not to do

  • Trusting php -v. That’s the console PHP, not necessarily the one serving the site.
  • Updating the package and not restarting. The old workers keep running the vulnerable code.
  • Applying the mitigations with ini_set() or .user.ini. The attack happens before they’re read.
  • Looking for 8.2.35 on Debian or RHEL. Look for the CVE in the changelog.
  • Downloading “exploits” from GitHub to test. Fake PoCs are already circulating, some with malware inside. The PHP team didn’t find a single working one among those that appeared after the leak.
  • Thinking the WAF is enough. It’s one more layer; the fix is the patch.

Summary

  1. php2shell is two PHP bugs that allow unauthenticated code execution; they affect FPM, mod_php, CGI, FrankenPHP and LiteSpeed.
  2. Take a real inventory: packages, running processes, containers and software that bundles PHP.
  3. Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or your distro’s package that lists the CVEs) and restart every worker.
  4. If you can’t today, disable session.upload_progress, drop the memory_limit with a cgroup cap and put a WAF in front; anything without a fix goes off the internet.
  5. Review what the web user has launched since October 20.

The worst part of bugs like this isn’t the patch, which takes ten minutes: it’s the PHP nobody remembered they had. Today’s inventory will come in handy for the next one.

Mini quiz

Did it stick?

Three quick questions. Each right answer is worth 10 XP.

  1. On a Debian server you run `php -v` and see a patched version. Can you consider the website safe?
  2. You want to disable `session.upload_progress` until the patch lands. Where do you set it?
  3. Your distro ships the fix, but the package still says 8.2.29 instead of 8.2.35. What does that mean?
  • #php
  • #security
  • #vulnerabilities
  • #linux
  • #apache
Esc