Cybersecurity Tutorial

PHP2Shell Fix: Patch PHP on Linux, Docker and Windows

PHP2Shell Fix: Patch PHP on Linux, Docker and Windows

This guide shows how to fix PHP2Shell (CVE-2026-109618 and CVE-2026-109619) on a real server: find out which PHP actually serves your site, update it on Debian, Ubuntu, RHEL/AlmaLinux/Rocky, Docker and Windows, verify that the fix is live, apply mitigations if you cannot update right away, and hunt for signs of attack. For background on the vulnerability, see the analysis: PHP2Shell: Critical PHP RCE (CVE-2026-109618) Explained.

The fixed versions released by the PHP project on 21 October 2026 are 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (php.net). Linux distributions apply the same fix to their own packages with different version numbers: the table in step 3 lists the ones to look for.

Time required: 15–30 minutes per server. You need root or sudo access.

Step 1 – Find out which PHP serves your site

Many servers run several PHP versions side by side, and php -v only shows the command-line (CLI) one, which may differ from the one used by the web server. You need to identify the SAPI in use (mod_php or PHP-FPM) and its package.

# CLI version (a hint, not the answer)
php -v

# Apache with mod_php? (Debian/Ubuntu: apache2ctl, RHEL: httpd)
apache2ctl -M 2>/dev/null | grep -i php
httpd -M 2>/dev/null | grep -i php

# Running PHP-FPM services
systemctl list-units --type=service --state=running | grep -i fpm

# Installed PHP packages
dpkg -l 'php*' 2>/dev/null | awk '/^ii/{print $2, $3}'   # Debian/Ubuntu
rpm -qa 'php*' 2>/dev/null | sort                          # RHEL/AlmaLinux/Rocky

The most reliable method is to ask PHP itself, through the web server. Create a temporary file with an unguessable name, open it in the browser and delete it immediately:

F="/var/www/html/check-$(openssl rand -hex 6).php"
echo '<?php echo PHP_VERSION, " | ", PHP_SAPI, " | upload_progress=", ini_get("session.upload_progress.enabled"), " | ", php_ini_loaded_file();' | sudo tee "$F"
echo "Open: https://yoursite.com/$(basename "$F")"
# once you have read it:
sudo rm -f "$F"

The output, for example 8.3.6 | fpm-fcgi | upload_progress=1 | /etc/php/8.3/fpm/php.ini, tells you the version, the SAPI and the php.ini your site really uses. apache2handler means mod_php: the highest-risk setup, to be patched first.

Step 2 – Update PHP on Debian and Ubuntu

Update every installed PHP package without touching the rest of the system:

sudo apt update
dpkg -l 'php*' | awk '/^ii/{print $2}' | xargs sudo apt-get install --only-upgrade -y

On Ubuntu 20.04 and older, fixes are only available through Ubuntu Pro (ESM): without an active subscription apt will find no updates. Enable it with sudo pro attach (free for personal use on up to 5 machines).

If you use Ondřej Surý’s packages (ppa:ondrej/php on Ubuntu, packages.sury.org on Debian), you get the upstream PHP versions: after updating, php -v must show at least 8.2.35, 8.3.36, 8.4.27 or 8.5.12. If the repository has not published them yet, apply the step 6 mitigations meanwhile.

Step 3 – Verify the fixed package on Debian and Ubuntu

Distributions backport the patch without changing the upstream version: a patched Ubuntu 24.04 still shows PHP 8.3.6. Check the package version and the changelog:

# Installed version (replace 8.3 with your branch)
dpkg -l php8.3-common | awk '/^ii/{print $2, $3}'

# The changelog must mention both CVEs
zcat /usr/share/doc/php8.3-common/changelog.Debian.gz | grep -m2 -E 'CVE-2026-10961[89]'

Minimum fixed versions, from the Debian Security Tracker and Ubuntu Security:

Distribution Package Fixed version (or later)
Debian 13 trixie php8.4 8.4.27-1~deb13u1 (DSA-6574-1)
Debian 12 bookworm php8.2 8.2.35-1~deb12u1
Ubuntu 26.10 php8.5 8.5.9-0ubuntu2.1
Ubuntu 26.04 LTS php8.5 8.5.4-0ubuntu1.5
Ubuntu 24.04 LTS php8.3 8.3.6-0ubuntu0.24.04.13
Ubuntu 22.04 LTS php8.1 8.1.2-1ubuntu2.28
Ubuntu 20.04 LTS (Pro/ESM) php7.4 7.4.3-4ubuntu2.29+esm5
Ubuntu 18.04 LTS (Pro/ESM) php7.2 7.2.24-0ubuntu0.18.04.17+esm14

Step 4 – Update PHP on RHEL, AlmaLinux and Rocky Linux

sudo dnf clean metadata
sudo dnf upgrade 'php*'

# Verify: package version and changelog
rpm -q php-common
rpm -q --changelog php-common | grep -m2 -E 'CVE-2026-10961[89]'

On AlmaLinux 10 with PHP 8.4 the packages are named php8.4-* (check with rpm -q php8.4-common). Here too the upstream version stays the distribution’s own: AlmaLinux, for example, fixed PHP 8.3 in package php-8.3.33-1.el10_2.alma.1 (AlmaLinux). On RHEL 9 the php:8.2 module moves to 8.2.35.

On AlmaLinux/RHEL 8 the default stream is PHP 7.2, out of support for years: this is the time to move to a recent stream.

dnf module list php
sudo dnf module reset php
sudo dnf module enable php:8.2
sudo dnf distro-sync 'php*'

Test a major version change in staging first: old extensions and applications may not be compatible.

If you use the Remi repository: as of 21 October it had not published updates yet, and the PHP project suggests switching to distribution packages or building from source (Help Net Security). Check with dnf check-update 'php*' and, until the patch lands, apply step 6.

Step 5 – Restart the workers (mandatory)

Updating the files on disk is not enough: PHP processes already running keep using the vulnerable code until they restart. With PHP2Shell this matters twice over, because CVE-2026-109619 abuses exactly the state left inside workers.

# Debian/Ubuntu (one service per installed version)
sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2          # if mod_php, or Apache in front of FPM

# RHEL/AlmaLinux/Rocky
sudo systemctl restart php-fpm
sudo systemctl restart httpd

# Check: process start time must be after the update
ps -eo pid,lstart,cmd | grep -E 'php-fpm|apache2|httpd' | grep -v grep

Then repeat the web check from step 1 (and delete the file again).

Docker

The official php images build the upstream version: rebuild with a fixed tag and recreate the containers.

# Pin the fixed version in your Dockerfile
FROM php:8.4.27-fpm
docker compose build --pull
docker compose up -d
docker compose exec app php -v     # must show 8.4.27 or later

Third-party images (ready-made apps, LAMP stacks) must be updated once the vendor ships a new release: check the PHP version inside the container with php -v.

Windows (IIS or Apache)

Download the fixed build from windows.php.net, replace the PHP folder while keeping your php.ini, and restart the service: iisreset for IIS (or recycle the application pool), restart the Apache service otherwise. On IIS, PHP runs as php-cgi through FastCGI, one of the modes listed as exposed.

Shared hosting and control panels

On managed hosting, cPanel or Plesk, updating PHP is up to the provider or the panel. Check the version with the step 1 test file and, if it is not fixed, open a ticket asking for explicit confirmation of the patch for CVE-2026-109618 and CVE-2026-109619.

Step 6 – Mitigations if you cannot update yet

Mitigations reduce the risk but do not close the hole: the PHP project states that the only complete mitigation is updating (GHSA-6wmf-4rv5-p3rp). If the service is not essential, several national CERTs advise taking it offline until patched.

Settings must be applied administratively: ini_set() and .user.ini do not work, because the attack happens before those values are read.

PHP-FPM, in the pool file (e.g. /etc/php/8.3/fpm/pool.d/www.conf):

; Removes the confirmed trigger of CVE-2026-109619 (keep it after patching if unused)
php_admin_flag[session.upload_progress.enabled] = off

; Conservative request limits
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M

; PHP 8.5 only
php_admin_flag[fatal_error_backtraces] = off

; Optional: one request per worker, no reusable state (costs performance)
pm.max_requests = 1

Apache with mod_php, in the VirtualHost or apache2.conf:

php_admin_flag session.upload_progress.enabled off
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 6M
LimitRequestBody 8388608
LimitRequestFields 50

memory_limit = -1 removes the simplest trigger of CVE-2026-109618, but on its own exposes the server to RAM exhaustion. If you use it, pair it with a service memory limit, for example with sudo systemctl edit php8.3-fpm:

[Service]
MemoryMax=2G

Do not use RLIMIT_AS or RLIMIT_DATA (for example ulimit -v): according to the advisory they cause exactly the allocation failure that makes the flaw exploitable (GHSA-w8hw-w9fx-j79w).

On an Nginx reverse proxy: cap request size and, where the application has no uploads, block multipart requests:

client_max_body_size 8m;

# Only where uploads are not needed: block multipart POSTs
if ($http_content_type ~* "multipart/form-data") {
    return 403;
}

After each change: sudo nginx -t, sudo apachectl configtest, then restart PHP and the web server. With mod_php, check the effective value with the step 1 test file.

Read also: Nginx Reverse Proxy: Secure HTTPS, Headers, and Rate Limiting

Step 7 – Hunt for signs of attack

Details circulated before disclosure and an exploitation attempt was seen on 17 October (Help Net Security): on servers that stayed exposed, look for the signals listed by Italy’s ACN. Adjust log paths to your system.

# Out-of-memory errors (CVE-2026-109618 trigger)
sudo grep -h "Allowed memory size of" /var/log/php*fpm*.log /var/log/apache2/error.log* \
     /var/log/httpd/error_log* /var/log/nginx/error.log* 2>/dev/null | tail -n 50

# PHP worker crashes
sudo grep -hE "exited on signal (11|6)|SIGSEGV" /var/log/php*fpm*.log 2>/dev/null
sudo grep -h "Segmentation fault" /var/log/apache2/error.log* /var/log/httpd/error_log* 2>/dev/null
sudo journalctl -k --since "2026-10-15" | grep -iE "segfault.*(php|apache2|httpd)"

# POSTs that ended in 500/502 (combined log format)
sudo awk '$6 ~ /POST/ && ($9 == 500 || $9 == 502)' /var/log/nginx/access.log /var/log/apache2/access.log 2>/dev/null | tail -n 50

# Processes spawned by the web server (shells, wget, curl, python...)
ps -eo user,pid,ppid,lstart,cmd --forest | grep -A3 -E "php-fpm|apache2|httpd" | grep -vE "php-fpm|apache2|httpd|grep"

# PHP files created or modified recently in the webroot
sudo find /var/www -type f -name "*.php" -newermt "2026-10-15" -printf "%TY-%Tm-%Td %TH:%TM %p\n" | sort

# Outbound connections of web processes
sudo ss -tpn state established | grep -E "php-fpm|apache2|httpd"

A few isolated memory errors are normal on busy sites; what matters is bursts from the same IP address, crashes clustered within minutes, child processes that should not exist (a shell under www-data) and PHP files appearing in upload folders. If you see these signals, treat the server as compromised: isolate it, preserve logs and a disk image, rotate the credentials and keys used by the applications.

Read also: ACN Incident: 24-Hour Notification Checklist

Final checklist

  • Every server and container with exposed PHP identified, with SAPI and version
  • PHP packages (or Docker images) updated
  • Package version and changelog with both CVEs verified
  • PHP-FPM and/or Apache restarted; process start time checked
  • Version and SAPI confirmed through the web; test file deleted
  • session.upload_progress.enabled turned off if the application does not use it
  • Logs reviewed from 15 October onward
  • Migration planned for servers on unsupported PHP (8.1 and older)

Read also: Linux Hardening: 15-Point Checklist for Production (2026)

Sources

Share this article:

Written by

Rosario Giordano

Rosario Giordano is a system administrator and IT consultant specializing in cybersecurity and cloud, with over 20 years of experience managing enterprise Linux infrastructures. His areas of expertise include SSH hardening, Kubernetes platforms, PostgreSQL databases, VMware/ Proxmox virtualization, and compliance with NIS2 and ISO 27001 security frameworks