All articles

Security

PHP2Shell: the PHP vulnerability and how to fix your server

PHP2Shell (CVE-2026-109618, CVE-2026-109619) lets attackers run code on PHP servers. How to tell if you're exposed, patch properly and check your logs.

By Madhumita S19 min read

PHP2Shell is a pair of critical bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619, that can let an attacker with no account run code on a server that uses PHP to answer web requests. The fix is PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, released on October 21, 2026, or your Linux distribution's patched package, followed by a restart of every PHP process. Attackers were probing for the bugs before the patches were out, and on October 26 CISA added both to its catalog of known exploited vulnerabilities.

Updating WordPress, Laravel or a plugin won't fix it, because the bugs are in the PHP runtime underneath them.

This guide is for whoever looks after a PHP server: a developer, a small team's part-time sysadmin, or the owner of a WordPress site on a VPS. We checked every fact in it against PHP's advisories and the original research on October 30, 2026. The story is still moving, so the linked sources are worth a look if you're reading this later.

The short version:

  1. Find the PHP version that actually serves each site, not the one php -v reports.
  2. Install 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's fixed package.
  3. Restart every PHP process and check the version again.
  4. Turn off session.upload_progress unless a site uses it.
  5. Check your logs from October 17 onward.

What PHP2Shell is

Both bugs come from the same place: what PHP does when a request is interrupted part-way through.

The first, CVE-2026-109618, is in out-of-memory handling. When PHP runs out of memory, it jumps straight out of whatever it was doing. PHP's advisory explains that at some points, for example while PHP is still parsing an uploaded form, that jump leaves the request's internal data half-built, and the cleanup that runs afterwards treats it as valid. An attacker can cause this on purpose. Each part of a multipart upload can carry any number of headers, and those take far more memory inside PHP than on the wire, so a request well under post_max_size can still exhaust memory_limit.

The second, CVE-2026-109619, is about state that outlives its request. A PHP worker handles one request after another, and some of its internal error, session and extension state can point into memory owned by the previous request. If a request dies before that state is reset, the next request on the same worker can use it after the memory has been freed. With a carefully arranged heap, that leads to running code, and on the way it can leak memory contents to the attacker.

PHP rates both 9.2 out of 10 on CVSS v4. The 10.0 that circulated before the release was wrong, according to the PHP Foundation. Calif, the security firm that reported the first bug on August 11, says the earliest affected code dates back to PHP 5.4.0. Wiz reported another attack path on August 24 and OpenAI reported more on September 18.

How the release went, and how worried to be

The fix was meant to ship on October 22. It came a day early because details started leaking:

Date (2026, UTC)What happened
August 11Calif reports the first bug to PHP.
October 17Amazon's honeypots log the first exploit attempt, four days before disclosure.
October 20, ~15:00The two CVE numbers circulate on forums and social media. PHP posts its advance notice at 19:30.
October 21, 07:41Chainguard publishes patched images, six hours before the coordinated release.
October 21, 14:00PHP releases 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
October 23PHP warns of a third, unpatched issue, CVE-2026-115560.
October 24A researcher confirms 35 successful attacks on deliberately weak honeypots.
October 26CISA lists both CVEs as exploited; US federal civilian agencies had until October 29 to act.

So far there are far more attempts than confirmed break-ins. Fastly counted about 250,000 requests from PHP2Shell tools, from about 3,200 IP addresses, by midday on October 22. Wordfence, which protects more than 5 million WordPress sites, logged about 77,600 related requests from over 2,000 addresses between October 23 and 27. As of October 28 it knew of no WordPress site compromised this way. Sophos claimed one successful attack on October 22 and retracted it the same evening.

The confirmed successes are narrower. Researcher Adel Karimi recorded 35 shells started by Apache on honeypots running PHP 8.4.26 under single-worker mod_php, a setup he describes as "intentionally favorable". Palo Alto Networks reports active exploitation of PAN-OS firewalls whose management interface is exposed to the internet. On October 26, Dream reported a chained attack using a tailored PHP2Shell exploit and a Linux kernel flaw to compromise 24 internet-facing firewalls and an internal management server; on October 28, Morphisec reported code execution on a customer Windows/IIS server running PHP-CGI. Neither report publishes enough technical detail to verify independently, and neither involves WordPress.

Our read: a reliable exploit is hard to build. Wordfence says the public exploit code it, Wiz and Hacktron reviewed "generally needed changes for the server being targeted". That buys you days, not safety. Exploits get better, and on mod_php the attack doesn't depend on your application at all. Treat this as a job for today.

Which setups are most exposed

PHP's advisory covers every installation that serves web requests, and the two GitHub advisories go further, setup by setup:

How PHP runsWhat PHP's advisories say
Apache with mod_phpHighest risk. Unauthenticated code execution was demonstrated over HTTP with default limits, before any application code runs.
PHP-FPM (Nginx, or Apache with proxy_fcgi)FPM ends a worker whose startup fails, which blocks the pre-application path. Code execution through session upload progress (CVE-2026-109619) was still demonstrated, and Calif shows a working exploit against Nginx with PHP-FPM.
FrankenPHPLikely open to the attack that doesn't need application code, judging from PHP's reading of the source.
LiteSpeed (LSAPI)Built like FPM. Exploitation through application code is likely but unproven.
php-cgiListed as at risk; no separate analysis.

Two groups of people can't fix this on their own servers:

  • Shared and managed hosting. Your host controls PHP, and you can't apply the stopgaps below yourself, because .user.ini and ini_set() take effect too late. Wordfence suggests asking your host exactly this: "Is the PHP service serving my site patched for CVE-2026-109618 and CVE-2026-109619, and has PHP been restarted?" In WordPress, Tools › Site Health › Info › Server shows the PHP version your site really runs.
  • Appliances with PHP inside. Firewalls and gateways often run PHP behind their web interface. Cisco lists five affected product rows, including Expressway and Duo Access Gateway, and Palo Alto lists PAN-OS. Those need the vendor's update, not a PHP package, so check the advisories for anything you run that has a web admin panel.

Step 1: find the PHP that actually serves your site

php -v tells you about the command-line PHP. The web server can be running a different one: a separate FPM package, an older version kept for one site, a container image, or a copy bundled with a control panel. Ask the web server instead. Put a file with a random name in the site's web root:

<?php echo PHP_VERSION, ' ', PHP_SAPI;

Open it in a browser, note the result, and delete the file. PHP_SAPI tells you how PHP runs: apache2handler is mod_php, fpm-fcgi is PHP-FPM, litespeed is LSAPI, frankenphp is FrankenPHP and cgi-fcgi is CGI or FastCGI.

One server can run several PHP versions, so check every site. From a shell, these list what's installed and running:

ps -eo user,args | grep -E '[p]hp-fpm|[l]sphp|[f]rankenphp'  # PHP processes
apachectl -M 2>/dev/null | grep -i php                      # mod_php loaded?
dpkg -l 'php*' | grep '^ii'                                 # Debian, Ubuntu
rpm -qa 'php*'                                              # RHEL, AlmaLinux, Rocky

One catch: distributions keep the old version number. Debian, Ubuntu and Red Hat patch the PHP version they originally shipped, so a fully patched system can still print an older number. On those systems, compare the package version against your distribution's advisory in the table below, not the number PHP prints.

Step 2: install the fixed PHP from wherever yours came from

The version in the table is the minimum fixed package for the release named. If your package manager offers a later build, take it. If PHP came from a third-party repo, a control panel such as cPanel or Plesk, or a vendor container, the distribution row does not apply: wait for that vendor's fixed build, or move to the distribution or upstream build. Do not assume an old-looking version is safe just because it has a recent build date.

Where PHP came fromMinimum fixed package versionAdvisory and update
Debian 13 (trixie)php8.4 8.4.27-1~deb13u1DSA-6574-1; sudo apt update && sudo apt full-upgrade
Debian 12 (bookworm)php8.2 8.2.35-1~deb12u1DLA-4848-1; sudo apt update && sudo apt full-upgrade
Ubuntu 26.10php8.5 8.5.9-0ubuntu2.1USN-8948-1; sudo apt update && sudo apt full-upgrade
Ubuntu 26.04 LTSphp8.5 8.5.4-0ubuntu1.5USN-8948-1; sudo apt update && sudo apt full-upgrade
Ubuntu 24.04 LTSphp8.3 8.3.6-0ubuntu0.24.04.13USN-8948-1; sudo apt update && sudo apt full-upgrade
Ubuntu 22.04 LTSphp8.1 8.1.2-1ubuntu2.28USN-8948-1; sudo apt update && sudo apt full-upgrade
RHEL 10php 8.3.36-1.el10_2 and php8.4 8.4.27-1.el10_2RHSA-2026:87070, RHSA-2026:87071; sudo dnf update --security
RHEL 9php 8.0.30-8.el9_8.1; php:8.2 stream at 8.2.35; php:8.3 stream at 8.3.36RHSA-2026:87151, RHSA-2026:87072, RHSA-2026:87079; sudo dnf update --security
RHEL 8php:8.2 stream at 8.2.35; php:7.4 stream at 7.4.33-8RHSA-2026:87073, RHSA-2026:87080; sudo dnf update --security
AlmaLinux and Rocky Linux 8, 9 and 10The matching RHEL rebuild packages, including the module streams aboveCheck the rebuild's AlmaLinux errata or Rocky Linux errata for the ALSA/RLSA that carries the same fixes; sudo dnf update --security
Fedora 43php 8.4.27-2.fc43FEDORA-2026-d5f8446449; sudo dnf upgrade --security
SUSE Linux Enterprise 16.1php8 8.5.12-160100.1.1SUSE CVE-2026-109618; sudo zypper patch
SUSE Linux Enterprise 16.0 and openSUSE Leap 16.0php8 8.4.27-160000.1.1SUSE CVE-2026-109618; sudo zypper patch
SUSE Linux Enterprise 15 SP7php8 8.3.36-150700.3.24.1SUSE CVE-2026-109618; sudo zypper patch
SUSE Linux Enterprise 15 SP6php8 8.2.35-150600.3.39.1SUSE CVE-2026-109618; sudo zypper patch
openSUSE Tumbleweedphp8 8.5.12-1.1SUSE CVE-2026-109618; sudo zypper dup
Alpine Linux 3.22php82 8.2.35-r0Alpine CVE-2026-109618; sudo apk upgrade
Alpine Linux 3.23 and 3.24php83 8.3.36-r0, php84 8.4.27-r0 and php85 8.5.12-r0Alpine CVE-2026-109618; sudo apk upgrade
Amazon Linux 2023php8.2 8.2.35-1.amzn2023.0.1, php8.3 8.3.36-1.amzn2023.0.1, php8.4 8.4.27-1.amzn2023.0.1, php8.5 8.5.12-1.amzn2023.0.1ALAS2023-2026-3301, -3302, -3303, -3304; sudo dnf update --security
Amazon Linux 2, PHP 8.2 Extraphp 8.2.35-1.amzn2.0.1ALAS2PHP8.2-2026-015; sudo yum update --security
Official Docker imagesphp:8.5.12, php:8.4.27, php:8.3.36 or php:8.2.35 (also the -apache, -fpm and -cli variants)Rebuild with docker build --pull or docker compose build --pull, then recreate the containers
Source builds and Windows binariesPHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35; Windows builds carry a -1 suffixPHP release archive

For an unfamiliar package source, look up the CVE in that vendor's security tracker before you trust php -v. On Debian and Ubuntu, dpkg-query -W -f='${Package} ${Version}\n' 'php*' confirms what is actually installed. On the RPM family, rpm -q php php-fpm php-cli and dnf module list php show the package and stream in use.

Step 3: restart every PHP process, then check again

A package update replaces files on disk. PHP processes that are already running keep the old code in memory until they're replaced, so a server with the new package installed and the old processes running is still vulnerable.

sudo systemctl restart php8.3-fpm   # Debian, Ubuntu: one service per installed version
sudo systemctl restart php-fpm      # RHEL, AlmaLinux, Rocky
sudo systemctl restart apache2      # mod_php (the service is httpd on RHEL)

Some PHP processes survive a web server restart, or never get one:

  • LiteSpeed runs PHP in detached mode, where PHP processes "continue running independently, even when OpenLiteSpeed restarts". touch /usr/local/lsws/admin/tmp/.lsphp_restart.txt restarts them the next time they're needed, and killall -9 lsphp stops them at once.
  • Containers restart from the image they were built from. Rebuild with docker build --pull (or docker compose build --pull), then recreate the containers. Check that the Dockerfile's FROM line doesn't pin an older patch release.
  • Long-running PHP applications (Laravel Octane, RoadRunner, Swoole, FrankenPHP's worker mode, queue workers under Supervisor) keep running until something stops them. Restart those too.

Then run the Step 1 check again. If the version hasn't changed, something is still running the old build.

The fix also changes how PHP behaves when a script runs out of memory. It now stops the script at the next safe point instead of jumping out at once. If memory use keeps climbing past a second, higher threshold, the process exits immediately, so shutdown functions and error handlers don't run. A memory-hungry import job may fail differently than it used to.

If you can't update today: the stopgaps and their limits

PHP's advisory lists stopgaps. Each one closes a known attack path, and in PHP's words they "do not make an installation safe". Use them for hours or days while you get the update in.

They only work where PHP reads them before it parses a request: php.ini, the FPM pool, or the Apache configuration. .user.ini and ini_set() are too late.

; php.ini
session.upload_progress.enabled = 0
post_max_size = 8M           ; 32M if the site takes uploads; keep upload_max_filesize below it
fatal_error_backtraces = 0   ; PHP 8.5 only
memory_limit = -1            ; only with a container or cgroup memory cap, see below

In an FPM pool the same settings look like php_admin_flag[session.upload_progress.enabled] = off and php_admin_value[memory_limit] = -1. Under mod_php, use php_admin_flag and php_admin_value in the Apache configuration, which neither .htaccess nor ini_set() can override. Restart every worker afterwards.

Here's what each one does, and what it costs:

  • session.upload_progress.enabled = 0 blocks the confirmed session paths of CVE-2026-109619 and also covers CVE-2026-115560. Few sites use upload progress, and PHP suggests leaving it off after patching if you don't need it. This is the one we'd keep.
  • memory_limit = -1 removes the memory limit the CVE-2026-109618 exploit relies on. A lower-level allocation failure can still reach the bug, and a runaway script can now eat all of the server's memory. Pair it with a container or cgroup memory limit, and know that a cgroup kill can take down the FPM master and every worker unless each worker has its own group. Don't use ulimit -v or other RLIMIT_AS and RLIMIT_DATA limits: they make allocations fail, which is exactly what the exploit needs. Put a finite memory_limit back once you've patched.
  • Small request limits (post_max_size, plus size limits at the web server or proxy) reduce how much memory one request can make PHP use. The limits must also cover chunked requests, which have no Content-Length. If a site never accepts multipart/form-data posts, block them at the front end.
  • pm.max_requests = 1 in an FPM pool gives every request a fresh worker, so no state carries over. It costs throughput.
  • Hiding errors does nothing. The @ operator, display_errors and error_reporting act after the vulnerable code has run. Remove deprecated or invalid PHP settings applied per request (in .htaccess, for example), because the warnings they raise early are one of the confirmed paths.

A web application firewall helps, with caveats. Cloudflare has blocked known exploit patterns since 03:02 UTC on October 21 on every plan with its WAF, including Free. Paid plans should confirm the rules are in block mode, and sites that changed session.upload_progress.name aren't covered. AWS added PHP2Shell_BODY and PHP2Shell_BODY_Extended rules to its PHP managed rule group, but behind an Application Load Balancer they inspect only the first 8 KB of the request body. The exploit requests Karimi captured were 279 KB to 33 MB. Akamai and Fastly have rules as well, and Fastly customers have to opt in to its protection.

All of these vendors say to patch anyway. And a WAF only sees the traffic that goes through it. If your server's own IP address answers web requests, an attacker can go around the WAF, so let the server accept web traffic only from your CDN's address ranges.

CVE-2026-115560: the October releases don't fix this one

On October 23, PHP published an informational advisory about a third path to running code. It only applies when all of these are true:

  • the configuration sets session.serialize_handler to php_binary or php_serialize;
  • the application switches it to php with ini_set() before calling session_start();
  • session upload progress is still enabled.

PHP calls the combination unusual and will fix it in the next regular release, with a full advisory then. Until that release, the fix is the same setting as above: session.upload_progress.enabled = 0. If you already set it for CVE-2026-109619, you're done.

Check whether anyone got in before you patched

If you think a server may already be compromised, snapshot its disk before you change anything, if your host makes that easy. Restarting and updating destroys evidence you may want later.

Then look at the PHP and web server error logs from October 17 onward. These are the kinds of entries worth searching for. The first three appear in the honeypot report or PHP's advisory; zend_mm_heap corrupted is another PHP heap-corruption signature.

Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes) in Unknown on line 0
PHP Request Startup: Session ID is too long or contains illegal characters
AH00052: child pid 4321 exit signal Segmentation fault (11)
zend_mm_heap corrupted

"Unknown on line 0" means PHP ran out of memory before any script started, which a normal page load almost never does. 134217728 bytes is the default 128 MB limit, so on your server the number matches your own memory_limit.

In the access log, look for multipart POST requests to / or /index.php with no User-Agent, very large bodies and a PHP_SESSION_UPLOAD_PROGRESS field. Some scanners announce themselves as php2shell-scan/109618. CISA has published a Sigma detection rule if you run a log platform that reads them.

On the server itself, the signs of a successful attack are the web server's user starting a shell, curl or wget, and new PHP files in upload, plugin or theme folders. Wordfence saw attackers try to write a file called wp2s.php and read .env and wp-config.php. A first pass:

sudo grep -rIh "Unknown on line 0" /var/log | tail -n 20
sudo find /var/www -name '*.php' -newermt 2026-10-17 -ls
ps -u www-data -o pid,ppid,lstart,args   # the user is apache or nginx on RHEL
sudo crontab -l -u www-data

Deploys change files too, and attackers can reset timestamps, so find gives you a list to review, not a verdict. A clean log isn't proof either; PHP's advisory says so directly.

If you find signs of a break-in, rebuild the server from a clean source rather than cleaning it in place. Then rotate every secret the web server's user could read: database passwords, API keys in .env or wp-config.php, application keys and WordPress salts. We'd rotate those even without finding a shell if an unpatched mod_php server shows the attack pattern in its logs, because the exploit chain leaks memory to the attacker before it runs any code.

Running PHP 8.1 or older

The PHP project no longer updates 8.1 and older. Given the severity, its security team made an exception and published minimal source patches for 7.4, 8.0 and 8.1, for people who build PHP from source. Older versions get nothing from PHP, though some distributions and paid long-term support vendors backport fixes.

Those branches are source trees, not new official releases or binary packages. Build the branch you need, rebuild your extensions against it, and install it in place of the old build while keeping the old extension API in mind. If you don't build PHP, use a vendor backport:

Old branchFixed options as of October 30, 2026
PHP 7.4Build the PHP-7.4-CVE-2026-109618-109619 branch; or use RHEL 8 php:7.4 7.4.33-8, SUSE php74 7.4.33-1.74.1 / php7 7.4.33-150400.4.81.1, or Ubuntu Pro 20.04 php7.4 7.4.3-4ubuntu2.29+esm5.
PHP 8.0Build the PHP-8.0-CVE-2026-109618-109619 branch; or use RHEL 9 php 8.0.30-8.el9_8.1 or SUSE php8 8.0.30-150400.4.80.1.
PHP 8.1Build the PHP-8.1-CVE-2026-109618-109619 branch; or use Ubuntu 22.04 php8.1 8.1.2-1ubuntu2.28.
PHP 5.4–7.3No fix from the PHP project. Some paid LTS offerings still carry fixes: Ubuntu Pro has php5 5.5.9+dfsg-1ubuntu4.29+esm18 on 14.04, php7.0 7.0.33-0ubuntu0.16.04.16+esm22 on 16.04, and php7.2 7.2.24-0ubuntu0.18.04.17+esm14 on 18.04. Red Hat lists a RHEL 7 Extended Lifecycle Support fix for CVE-2026-109619; check the vendor advisory for the exact coverage before relying on it.

Whichever route you choose, verify that the installed build contains both fixes, not merely a newer package date. A vendor-patched branch can still print an old PHP version.

Don't combine the emergency patch with a version upgrade. Patch within your current branch today, then plan the move as a normal project. PHP 8.2 gets its last security fixes on December 31, 2026, so if that's where you are, aim for 8.4 (supported to the end of 2028) or 8.5 (to the end of 2029).

What we'd change so the next one hurts less

Most of the work this week was finding out what ran where. Keep a list of every server and site that runs PHP, with its version and how PHP runs, and include staging and the old server nobody has switched off.

Make sure whatever installs your updates also restarts PHP. An installed fix that isn't running protects nothing. Subscribe to PHP's release announcements and your distribution's security list; the advance notice arrived a day before the release.

For new servers, choose PHP-FPM over mod_php. This time it was the difference between an attack that runs before your code and one that needs more help from the application. FPM also lets each site run as its own user. And switch off PHP features you don't use, starting with session upload progress.

None of this is specific to PHP. Security patches are most of what maintenance means after launch, which we cover in our guide to software development.

If you do one thing today, run the Step 1 check on every site you're responsible for. If you'd rather someone else kept track of patching and monitoring, it's part of our DevOps and cloud infrastructure work.