Skip to content

← Writing

engineering

PHP2Shell: Two Critical RCEs in PHP Core, and What to Do Today

· Jerwin Arnado · 7 min read ·

Two unauthenticated remote code execution flaws in PHP core, CVE-2026-109618 and CVE-2026-109619, are both rated CVSS v4.0 9.2. The fixes shipped today, 21 October 2026: update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Details leaked a day early and attacks started before the patch, so if you run PHP in production, patch tonight.

Updated 21 October 2026, after the release: fixed versions, root causes and the official mitigations from PHP’s security advisory.

The fixed versions

Branch Update to
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35
8.1, 8.0, 7.4 No official release. Use a vendor backport, PHP’s source-only backport branches, or upgrade.

Linux distributions and LTS vendors got the patches in advance, so your distro’s packages may already be out. Check before you build from source.

What happened

  • Two RCEs in PHP core. PHP itself is affected, not a framework or a package. Laravel, Drupal, WordPress, Magento: if it runs on PHP, it’s in scope.
  • Severity: CVSS v4.0 9.2 for both. Some early reports said 10.0; the official number is 9.2.
  • The disclosure leaked. Word of the bugs spread on 20 October, ahead of the planned release, along with a screenshot of an exploit tool called PHP2Shell credited to Calif.IO. watchTowr confirmed both bugs were real and reproduced CVE-2026-109618 in its lab.
  • Attacks started before the patch. Cloudflare reported early attacks against PHP sites at 07:43 UTC today and pushed WAF rules to all customers, including free plans. Some Dutch sites, including Tweakers and the NCSC, went offline as a precaution.
  • The fix shipped at 14:00 UTC (10:00 PM here in Manila), as scheduled.

Who is at risk

According to PHP’s advisory: every PHP installation serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. PHP considers the risk highest for mod_php, where a demonstrated exploit lets an unauthenticated request trigger the attack before any application code runs. Your framework’s middleware, auth and WAF-in-PHP never get a say.

What the bugs are

From the official GitHub advisories:

  • CVE-2026-109618, RCE via out-of-memory corruption (GHSA-w8hw-w9fx-j79w). Out-of-memory handling can interrupt PHP while internal request data is only partly initialized. Request cleanup then treats that invalid data as valid.
  • CVE-2026-109619, RCE via stale global state (GHSA-6wmf-4rv5-p3rp). Request-owned internal state can survive after its memory is freed. A later request on the same worker can use that stale state before normal initialization resets it.

Both are request-lifecycle bugs: they live in the code that sets up and tears down every request. That’s why every SAPI is affected and why the attack lands before your app does.

The part that hurts: PHP 8.1 and older

PHP only supports 8.2 and later, and 8.2 itself reaches end of life on 31 December 2026. If you’re on 8.1, 8.0 or 7.4, there’s no official release. Given how severe these bugs are, PHP’s security team did publish minimal source-only backport branches for 7.4, 8.0 and 8.1, for people who build from source or have no LTS vendor. Otherwise, ask your distro or hosting provider for a backport, or upgrade.

I’ve written before about pinning legacy projects to older PHP-FPM pools. That’s fine for local dev. Anything on an old pool that is reachable from the internet is now a liability.

Patch-day checklist

1. Inventory every PHP binary you run

Don’t just check one. Most servers I touch have more than one PHP:

php -v                                # CLI
ls /etc/php/ 2>/dev/null              # Debian/Ubuntu: every installed version
ps aux | grep -E 'php-fpm|frankenphp' # what's actually serving traffic
docker ps --format '{{.Image}}' | grep -i php

Remember the ones that hide: Docker images pinned to php:8.x-fpm, FrankenPHP, which embeds PHP inside its own binary (updating system PHP doesn’t touch it), and Laravel Sail or Vapor runtimes.

2. Update

  • Distro packages: apt update && apt upgrade / dnf upgrade. Vendors got the patches early, so check now.
  • Remi’s repo: the PHP Foundation warned that Remi’s repository would not publish updates on release day. If you rely on it, use your distribution’s packages or build from source until it catches up.
  • Homebrew: brew update && brew upgrade php (and each [email protected] you have).
  • Docker: pull the new official image tag and rebuild. Don’t trust a cached base layer.
  • FrankenPHP: upgrade the binary itself to a build that bundles one of the fixed versions.
  • Source / Windows: php.net downloads.

Then restart every worker (a reload isn’t enough for a new binary) and confirm the version from a web request, not the CLI. They’re often different binaries.

3. If you can’t update tonight

PHP is blunt about this: only an update is a complete solution. These official measures cut off the known attack paths but don’t make you safe.

For CVE-2026-109618 (OOM corruption):

  • Set memory_limit=-1 somewhere applications can’t override it, such as php_admin_value in the FPM pool or Apache config, and make sure no pool, directory or app setting puts a finite limit back. This removes the PHP memory-limit failure an attacker can trigger, but a real allocation failure can still reach the bug.
  • Set a low post_max_size: 8M if the app doesn’t take uploads, 32M if it does, and keep upload_max_filesize below it.
  • Enforce request limits at the web server or proxy as well: decoded body size, input field count, multipart part count, header count and nesting depth. Make sure they also cover requests without a usable Content-Length.
  • Use a container or cgroup memory limit as the safety net, ideally one that kills only the affected worker. Don’t use RLIMIT_AS or RLIMIT_DATA instead, because they can trigger the very allocation failure you’re trying to avoid.

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

  • Set session.upload_progress.enabled=0 in php.ini, an FPM pool or server config that applies before request parsing, then restart every worker. ini_set() and FPM .user.ini files don’t work here. This blocks the confirmed session paths, not every variant.
  • On PHP 8.5, also set fatal_error_backtraces=0 and restart.
  • Remove invalid or deprecated per-request settings that emit diagnostics during startup. Hiding the diagnostics with error_reporting, display_errors or @ doesn’t help.
  • Fix fatal errors and execution timeouts an attacker can trigger, including in callbacks, upload or import processing, and cleanup.

For PHP-FPM, the pool version looks like this:

php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_flag[session.upload_progress.enabled] = off

Edge stopgaps still help while you work: Cloudflare’s emergency WAF rules (with the origin locked to Cloudflare’s IP ranges, or attackers just go around it), or taking the site read-only behind a static maintenance page. Framework maintenance modes like php artisan down don’t help, because the attack lands before your application code runs.

4. Assume you might be too late

Details leaked yesterday and attacks were seen hours before the patch. After updating, look for anything that shouldn’t be there: new .php files in upload or cache directories, unexpected cron entries, unfamiliar processes running as www-data, and outbound connections from web servers that normally don’t make any.

The bigger lesson

Most of us treat PHP as plumbing: the thing under Laravel or Drupal that gets updated when the OS does. Today shows why it needs to be on the inventory like any other dependency. Know which versions you run, where they run, and how fast you can roll them. The teams that patched in fifteen minutes tonight were the ones that already knew the answer to step 1.

Sources