PHP2Shell (CVE-2026-109618 and CVE-2026-109619): what broke, what to upgrade, and the third bug the patch doesn't fix

On 2026-10-21 the PHP project shipped fixes for two critical remote code execution bugs in the PHP runtime itself, now known together as PHP2Shell. Both are CVSS 9.2, both work without authentication, and they affect every PHP release from 5.4.0 through 8.5.11. Two days later PHP disclosed a third RCE, CVE-2026-115560, which the PHP2Shell patches do not fix.

This post covers what each bug is, which versions to move to, how to check your servers quickly, and the one-line workaround for the third bug.

The two PHP2Shell bugs

CVE-2026-109618: RCE through out-of-memory corruption (CVSS 9.2). If PHP runs out of memory at the wrong moment while it is setting up request data, it leaves that data half built. The cleanup step then treats the half-built data as valid, which corrupts memory. An attacker who can push a request into that out-of-memory state can turn the corruption into code execution.

CVE-2026-109619: RCE through stale global state (CVSS 9.2). Some internal state that belongs to one request outlives the memory behind it. The next request handled by the same PHP worker then uses that freed memory. This matters most for long-lived workers (PHP-FPM, mod_php, FrankenPHP, RoadRunner, Swoole), which handle many requests per process.

What makes them serious:

  • They’re in the runtime, not in your code. The framework, CMS or code quality doesn’t matter. If PHP is vulnerable, the app is vulnerable.
  • No login needed. Both can be triggered by an unauthenticated HTTP request.
  • The affected range is huge. PHP 5.4 came out in 2012. Almost every PHP install still running is in range.
  • Working exploits exist. Public proof-of-concept and full-chain exploit repositories showed up within days of the release, and the bugs have been reported as added to CISA’s Known Exploited Vulnerabilities catalog. Treat them as being actively exploited.

Which version to upgrade to

BranchFixed inStatus
8.58.5.12Supported
8.48.4.27Supported
8.38.3.36Supported
8.28.2.35Security fixes only
8.1 and older (down to 5.4)No upstream fixEnd of life

If you are on 8.1 or older, no official patch exists. Your options are a vendor that backports fixes (a commercial extended-support vendor, or a distro package that still gets security backports), or migrating to 8.2+. Until then, take the server off the public internet or put it behind something that only lets trusted traffic in.

Check what you’re actually running

The PHP version your app uses is not always the one php -v reports. CLI, FPM, Apache’s module and each container can all be different builds.

php -v                                   # CLI
php-fpm -v 2>/dev/null || php-fpm8.3 -v  # FPM (binary name varies by distro)
ls /etc/php/                             # Debian/Ubuntu: every installed branch

for c in $(docker ps -q); do             # every running container that has PHP
  printf '%s: ' "$(docker inspect -f '' "$c")"
  docker exec "$c" php -r 'echo PHP_VERSION, PHP_EOL;' 2>/dev/null || echo "no php"
done

The surest answer is a temporary <?php echo PHP_VERSION; file served by the real web server. Delete it right after.

Distro packages: Debian, Ubuntu and RHEL often keep the old version number and backport the fix, so 8.3.6 from your distro may already be patched. Check your distro’s security tracker for the two CVE IDs rather than comparing version numbers with the table above.

Upgrading

Pick the line that fits your setup, then restart PHP-FPM / the web server. A running worker keeps the old code in memory until it restarts, and CVE-2026-109619 lives in exactly those long-running workers.

# Debian/Ubuntu (distro or ondrej/php packages)
sudo apt update && sudo apt install --only-upgrade 'php8.3*'
sudo systemctl restart php8.3-fpm

# RHEL/Alma/Rocky (Remi or AppStream)
sudo dnf upgrade 'php*'
sudo systemctl restart php-fpm

Docker: bump the image tag (php:8.3.36-fpm, php:8.4.27-apache, …), rebuild, and redeploy. A floating tag like php:8.3-fpm only helps if you pull it again and rebuild, because the image cached on your build host or CI runner is still the old one. Check the result with docker run --rm your-image php -v.

cPanel/WHM, Plesk and managed hosting: EasyApache / the panel ships its own PHP builds. Apply the panel’s critical security update, and check every PHP version the panel offers, not just the default one. Shared hosting accounts often stay pinned to an old branch.

CVE-2026-115560: the one the patch doesn’t fix

Disclosed on 2026-10-23, this is a separate RCE in session upload progress combined with changing session.serialize_handler at runtime. Updating to the PHP2Shell releases above does not close it. As of this writing PHP has not published a fixed version, a CVSS score or the affected ranges.

The bug needs all of this in the same request path:

  • session.serialize_handler is set to php_binary or php_serialize in config,
  • the app calls ini_set('session.serialize_handler', 'php') at runtime, before session_start(),
  • session.upload_progress.enabled is still 1 (the default).

Most apps don’t flip the serialize handler, but you probably don’t want to audit every plugin to find out. PHP’s own recommendation is to turn the feature off:

; php.ini, or a drop-in such as /etc/php/8.3/fpm/conf.d/99-cve-2026-115560.ini
session.upload_progress.enabled = 0

Restart PHP-FPM and confirm it took effect in the web context, not the CLI (they read different ini files):

php-fpm8.3 -i | grep upload_progress.enabled   # or check phpinfo() through the web server

The only thing you lose is the upload progress bar built on $_SESSION. Almost nobody uses it, since JavaScript upload progress (XMLHttpRequest.upload.onprogress / fetch streams) replaced it years ago. To find out whether your code uses either piece:

grep -rn --include='*.php' -e 'serialize_handler' -e 'upload_progress' /path/to/app

Checklist

  1. List every PHP runtime you run: hosts, containers, panels, CI images.
  2. Upgrade each to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, or the distro’s patched build.
  3. Restart FPM / the web server / the containers. Patching without restarting does nothing.
  4. Set session.upload_progress.enabled = 0 everywhere, whatever the version.
  5. Isolate or migrate anything on 8.1 or older.
  6. Look at web logs and running processes on anything that was exposed before you patched. Unauthenticated RCE with public exploits means “patched” doesn’t prove “not compromised.”
  7. Subscribe to the PHP announcements and your distro’s security feed. The second fix for CVE-2026-115560 still has to ship.

References

AM

Written by Agus Makmun Author

SDE Lead working across the whole stack: backend, frontend, databases and the infrastructure that ships them. These days mostly infrastructure and Next.js, like moving apps off Vercel and Supabase to self-hosted.

arrow_backPrevious post Mitigating CVE-2026-98432, a 9.8 Linux kernel TCP flaw, before a patched kernel existed
search esc