|

PHP2Shell: understanding the October 2026 PHP flaws and checking your servers

PHP2Shell is the name given to two PHP vulnerabilities published on 21 October 2026, CVE-2026-109618 and CVE-2026-109619. They allow, with a simple HTTP request and no authentication, running code on a server that runs PHP. With PHP powering about 70% of websites whose server technology is known (WordPress, Drupal, Joomla and a host of applications),…

PHP2Shell is the name given to two PHP vulnerabilities published on 21 October 2026, CVE-2026-109618 and CVE-2026-109619. They allow, with a simple HTTP request and no authentication, running code on a server that runs PHP. With PHP powering about 70% of websites whose server technology is known (WordPress, Drupal, Joomla and a host of applications), the reach is considerable: government sites, Wikimedia and the Internet Archive were taken offline as a precaution, and some experts compared it with Heartbleed.

This post explains what these flaws are, without going into exploitation details, starting from how PHP manages a request’s memory. It then describes how to check a server is really protected, which takes a little more than a version number, and what I did on my own servers. The facts come from the PHP project’s security advisories and the Wikipedia page about the affair.

The facts

CVE-2026-109618CVE-2026-109619
advisory title“RCE via OOM corruption”“RCE via stale global state”
naturememory corruption on out-of-memory during request cleanuprequest-owned internal state surviving after its memory was freed
CVSS 3.19.8 (no authentication, no interaction)8.1 (high attack complexity)
CVSS 4.09.29.2
affected versionsPHP 5.4 to 8.5PHP 5.4 to 8.5
fixed in8.2.35, 8.3.36, 8.4.27, 8.5.12same
Date (2026)Event
Augustdiscovery by two teams, helped by AI-assisted analysis tools; one of the bugs had been spotted in a 2024 audit, left unfixed for lack of agreement on the fix
17 Octoberfirst exploitation attempt seen by a honeypot, four days before publication
20 Octoberpublic announcement, leaks (a demo video released by mistake), fixes already visible at one distributor
21 October, 13:34 UTCadvisories and fixed versions published; exploitation observed the same day
23 Octoberfollow-up advisory CVE-2026-115560, tied to session upload progress

Two elements of this timeline deserve attention. The flaw was exploited before it was published: when the fix comes out, there’s no grace period any more. And the first bug had been spotted two years earlier, left unfixed for lack of a demonstration that it was exploitable: the arrival of tools able to build that demonstration changed everything. Expect this scenario to repeat with other old “theoretical” bugs.

Understanding: the life of a request in PHP

To grasp what “corruption on out-of-memory” and “stale global state” mean, you need to know how PHP handles a request, especially with PHP-FPM, the most common mode behind nginx.

  • reused processes: PHP-FPM starts a group of processes (workers), each handling one request after another, hundreds or thousands of times before being recycled (pm.max_requests). It isn’t a fresh process per request;
  • a four-step cycle: when the process starts, PHP initialises its extensions once and for all (the MINIT phase). Then for each request: request-specific initialisation (RINIT), script execution, then cleanup (RSHUTDOWN), which must return the process to a clean state for the next request;
  • per-request memory: PHP’s memory manager (the Zend Memory Manager) allocates the script’s memory in an area owned by the request, freed in one go at the end. It’s fast and avoids most leaks, but it assumes nothing keeps, beyond the request, a pointer into that area;
  • a memory limit: memory_limit caps a request’s memory. When a script exceeds it, PHP interrupts execution… in the middle of whatever it was doing, cleanup included.

The two bugs, at this level of explanation

The first bug concerns precisely that case: running out of memory during a request’s cleanup leaves some internal structures in an inconsistent, half-freed state. An attacker who knows how to trigger that exhaustion at the right moment, with a request built for it, gets a corruption of the process’s memory.

The second bug belongs to the same family: a piece of internal state belonging to a request isn’t reset and survives the release of its memory. The next request, handled by the same process, finds a reference to an area that was freed then reused, possibly filled by the attacker. It’s a use-after-free, a classic in programs written in C.

In both cases, controlled memory corruption makes it possible to hijack the PHP process’s execution flow, hence to run code with its rights: those of the web server user, who can read the sites’ files, their database credentials, and often write into their directories. The name “PHP2Shell” sums up the result.

Why every mode is affected

Both bugs are in PHP’s core, not in a particular module. PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, LiteSpeed: everything that serves web requests is concerned. The command line is less so, since no remote attacker sends it requests. And the fact that FPM processes are reused from one request to the next is precisely what makes state that “survives” a request exploitable.

Why a web application firewall isn’t enough

Web application firewall vendors published temporary rules within the first days. They have their use against published exploitation tools, but a memory corruption flaw triggered by the shape of a request lends itself poorly to filtering: varying the payload is often enough. PHP’s maintainers said it plainly: no workaround is complete, only updating protects. The US National Security Agency went as far as recommending taking offline the sites that couldn’t be updated immediately.

Checking a server: the version isn’t enough

php -v                                           # the command-line binary
rpm -qa 'php*-fpm' 'php*-common' --qf '%{NAME} %{VERSION} installed %{INSTALLTIME:date}\n'
systemctl show -p ActiveEnterTimestamp php-fpm   # did PHP-FPM restart AFTER the update?
needs-restarting -s | grep -i php                # is a process still using old libraries?
ls /opt/remi/*/root/usr/sbin/php-fpm 2>/dev/null # parallel versions (software collections)
  • the installed version must be at least 8.2.35, 8.3.36, 8.4.27 or 8.5.12. For older versions, no longer maintained by the project, only source patches exist; distributions and some vendors (cPanel, CloudLinux) packaged them;
  • the running process must have restarted after the update. A PHP-FPM started before installation keeps running the old code in memory, whatever package is on disk. needs-restarting -s lists the services concerned;
  • every copy of PHP: a server can have several (the main package, parallel collections in /opt/remi, containers bundling their own PHP). Each must be checked;
  • backported numbers: on a distribution that fixes without changing version (RHEL and derivatives for their stock PHP), it’s the package’s changelog you must read (rpm -q --changelog), not the version number.

What I found on my servers

# main
php-fpm 8.5.12  installed 21 Oct 2026 at 21:10   (advisory published at 15:34 Paris time)
# homelab
php-fpm 8.4.27  installed 21 Oct 2026 at 20:30

My two servers running PHP use packages from the Remi repository, which tracks official releases closely: the fixed versions were installed the very evening of publication, within five to six hours. One server’s PHP-FPM process had restarted since, and so had the other’s, but needs-restarting still flagged it for a library updated afterwards: one more restart settled that. It’s the check most often forgotten.

The follow-up flaw: upload progress

; 99-local.ini : disable session upload progress (CVE-2026-115560)
session.upload_progress.enabled = Off

On 23 October, the PHP team published an advisory on a third flaw, CVE-2026-115560, distinct from the first two: a configuration-dependent code execution tied to the session upload progress tracking feature. When it’s on (the default), PHP writes into the session, during an upload, information partly coming from the request. If no application uses it, disabling it costs nothing. I checked no PHP code on my sites used the feature (searching for PHP_SESSION_UPLOAD_PROGRESS and upload_progress), then disabled it through the common configuration deployed by Ansible.

Detecting an attempt

journalctl -u php-fpm --since "-7 days" | grep -E "exited on signal (11|6)|SIGSEGV|SIGABRT"
# WARNING: [pool www] child 12345 exited on signal 11 (SIGSEGV) after 3.2 seconds from start

A failed exploitation attempt against memory corruption often shows up as a crash of the targeted process. PHP-FPM processes dying on signal 11 (segmentation fault) or 6 (abort), especially in a series from the same address in the web server logs, are a signal to watch. An alert rule on those messages, in a tool like Wazuh, costs little.

The lessons

  • update speed matters more than ever: exploitation before publication, exploit tools within days. A package repository that tracks upstream releases closely, and automatic security updates, make the difference;
  • an update only applies when the process using the code restarts. Check it, don’t assume it;
  • end-of-life versions (PHP 8.1 and older) only got source patches. A server on an old version means compiling it yourself at the worst possible moment;
  • reduce what’s exposed: an unused feature (upload progress) disabled is one flaw fewer on the day it’s discovered;
  • and a deeper shift: AI-assisted analysis tools find and demonstrate old flaws faster than before. “Never exploited” bugs may not stay that way.

Sources: PHP project security advisories (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp); Wikipedia page “PHP2Shell”; Help Net Security and Recorded Future, October 2026.


This post is part of a series of sysadmin tutorials (tag “how-to”).
Series “How-to”, tutorial 1/45. · Next tutorial → (coming 30 Oct) · Version française

Similar Posts

One Comment

Leave a Reply

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