PHP2Shell: is your PHP application exposed, and what to do today

PHP2Shell: is your PHP application exposed, and what to do today

On 21 October 2026 the PHP project shipped emergency releases for two critical vulnerabilities, now known as PHP2Shell: CVE-2026-109618 and CVE-2026-109619. Both are rated 9.2 on CVSS v4.0, and both can let an unauthenticated request run code on your server. Researchers trace the vulnerable path back to PHP 5.4, and proof-of-concept exploits were public before the patch was.

The detail that matters most is this: your application does not need a file upload form to be exposed. The bug sits in how PHP itself handles multipart/form-data requests, before your framework or your code gets a say. If PHP serves web requests on your server, assume you are in scope until you have checked.

Here is what is affected, what to update to, and what to do on servers you cannot update today.

What PHP2Shell is, in plain terms

PHP has a feature called session upload progress, switched on by default since PHP 5.4. When a multipart POST arrives, PHP writes progress counters into the session as the body is parsed, whether or not the application ever reads them.

Researchers found that the handler, php_session_rfc1867_callback() in ext/session/session.c, keeps raw pointers into session memory. If an attacker stalls the upload at the right moment, that memory can be freed while PHP still holds the pointers, and later parts of the request write through them. That is a use-after-free, and with the right heap layout it becomes code execution.

The official advisory describes the two CVEs more broadly than the upload-progress path:

  • CVE-2026-109618, RCE via out-of-memory corruption. Out-of-memory handling can interrupt PHP while internal request data is only partly initialised, and cleanup then treats that data as valid.
  • CVE-2026-109619, RCE via stale global state. Request-owned state can survive after its memory is freed, and the next request handled by the same worker may use it.

The PHP team considers the risk highest for Apache with mod_php, where a demonstrated exploit fires before any application code runs. PHP-FPM, php-cgi, FrankenPHP and LiteSpeed are all affected too.

Which versions are fixed

  • PHP 8.5.12
  • PHP 8.4.27
  • PHP 8.3.36
  • PHP 8.2.35

PHP 8.1 and older get no official release. Because of the severity, the PHP security team published minimal source backports for 7.4, 8.0 and 8.1 for people who build PHP from source. Everyone else on an end-of-life version depends on their distribution or LTS vendor shipping a patched package, or on upgrading.

The mistakes we expect to see this week

Most PHP2Shell exposure will not come from people ignoring the news. It will come from people who acted on it and checked the wrong thing.

  • Checking php -v on the wrong binary. The CLI on a server is often a different build from the FPM pool or the Apache module that serves traffic. Check the binary that actually handles web requests (php-fpm8.3 -v, or a phpinfo() page behind authentication), on every host and every container image.
  • Reading distribution version numbers literally. Debian, Ubuntu and RHEL backport security fixes without changing the upstream version, so a patched package can still report 8.1.x or 8.2.x. Look for the CVE IDs in the package changelog, not for the number 8.2.35.
  • Updating the package and not restarting the workers. PHP-FPM and Apache keep the old code in memory until they restart. The same applies to every configuration mitigation below.
  • Rebuilding a Docker image from cache. If your Dockerfile starts from php:8.3-fpm, a cached build keeps the old base layer. Rebuild with --pull and redeploy.
  • Disabling upload progress in .user.ini or with ini_set(). Several early write-ups suggested it. The official advisory says not to rely on either: by the time they apply, the request body is already being parsed.

If you cannot update today

Updating is the only complete fix. The PHP team says so plainly, and states that each mitigation below blocks the confirmed paths, not every variant. Still, on a server you cannot patch this afternoon, these reduce exposure while you schedule the upgrade.

For CVE-2026-109619 (stale global state):

  1. Set session.upload_progress.enabled=0 somewhere that applies before request parsing: php.ini, the FPM pool (php_admin_flag[session.upload_progress.enabled] = off) or the Apache configuration. Then restart every worker.
  2. On PHP 8.5, also set fatal_error_backtraces=0 and restart.
  3. Remove invalid or deprecated settings applied per request that emit diagnostics before initialisation. Hiding them with error_reporting, display_errors or @ does not help.
  4. Fix fatal errors and execution timeouts an attacker can trigger, including in upload and import handling.

For CVE-2026-109618 (out-of-memory corruption):

  1. Lower post_max_size: 8M if the application takes no uploads, 32M if it does, with upload_max_filesize below it. Enforce request size, multipart part count and header count at the web server or proxy as well, including for requests without a usable Content-Length.
  2. The advisory also suggests memory_limit=-1, set with php_admin_value so applications cannot override it, combined with a container or cgroup memory limit that kills only the affected worker. Do not use RLIMIT_AS or RLIMIT_DATA instead; they can trigger the very allocation failure the bug needs. Removing the memory limit changes how a runaway request fails, so only do this with a real container limit in place.

The blunt option. If the application never accepts multipart POSTs, you can refuse them at the web server before PHP sees them. In Nginx:

if ($http_content_type ~* "multipart") {
    return 403;
}

This breaks every file upload and every form that sends multipart/form-data, including admin panels and CMS editors. Check before you deploy it, and scope it to the locations that do not need uploads where you can. It is the same idea as refusing scanner traffic in Nginx before it reaches PHP: the cheapest request to defend against is the one PHP never parses.

How to tell if someone tried

Exploits need build-specific memory offsets, and a wrong guess usually crashes the worker. That gives you a signal:

  • PHP-FPM log lines such as child exited on signal 11 (SIGSEGV), or Apache child segfaults, that do not match a deploy.
  • Bursts of 502 from Nginx in front of PHP-FPM, or dropped connections under mod_php, on POST requests.
  • Multipart POSTs carrying a PHP_SESSION_UPLOAD_PROGRESS field to URLs that have no upload form.

A successful exploit can return a normal 200, so a clean access log proves nothing. If you find crashes from before you patched, treat the host as possibly compromised: check for new files in web-writable directories, unexpected cron entries and outbound connections, and rotate the credentials the application holds.

A checklist for today

  1. List every host and container image that runs PHP for web traffic, including staging, cron hosts with a web endpoint, and forgotten admin tools.
  2. For each, record the SAPI (FPM, mod_php, CGI, FrankenPHP, LiteSpeed) and the real version of the binary serving requests.
  3. Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or install a vendor package whose changelog names both CVEs.
  4. Restart PHP-FPM, Apache and every worker pool, then confirm the new version from a web request.
  5. Where you cannot update today, apply the mitigations above at the server level and restart.
  6. Put every PHP 8.1-or-older system on a dated upgrade plan. This will not be the last critical bug without an official fix for them.
  7. Check the logs back to mid-October for worker crashes on multipart POSTs.

Where we fit

Patching PHP is easy when you know where it runs. It gets hard on inherited systems: a mod_php server nobody has logged into for two years, a Docker image pinned to php:7.4-apache, a framework version that will not run on 8.2. That is most of the work we do. This takeover case shows what it looks like in practice.

If you are not sure what your PHP estate looks like, our System Risk Check answers that in days: every runtime, its version and support status, and what to fix first. For applications stuck on old PHP, PHP application maintenance and Laravel maintenance and support cover the upgrade path, and what a legacy PHP audit covers explains how we assess one before touching it.

Sources: PHP security announcement, GitHub advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp, and Upwind's initial assessment. Checked 21 October 2026.

system risk check

Not sure what state your application is in?

A System Risk Check is a fixed-price technical audit of your application, database and infrastructure, with a written, prioritised report. The report is yours, whatever you decide next.