Skip to content

Blog · Security · Published 28 October 2026

PHP2Shell (CVE-2026-109618, CVE-2026-109619): How to Patch, Mitigate, and Verify

Two critical flaws in the PHP runtime itself — not in your application code — can allow unauthenticated remote code execution. Here is what to patch, how to find every PHP you run, and what to do if you cannot patch today.

Sources. Every technical fact in this post comes from the PHP project's own publications: the php.net release announcements of 20, 21, 23 and 26 October 2026, and the two official security advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp. Third-party reporting on exploitation is deliberately left out. If an advisory is updated after 28 October 2026, the advisory wins.

TL;DR

  • Upgrade to PHP 8.2.35, 8.3.36, 8.4.27 or 8.5.12 (released 21 October 2026), then restart every PHP worker.
  • The PHP project's own words: “There are no complete mitigations, updating is the only safe choice.”
  • Risk is highest on Apache mod_php; PHP-FPM, FrankenPHP and LSAPI are also in scope.
  • PHP 8.1 and older receive no fixes from the PHP project. Moving to a supported branch is the fix.
  • If you don't use session upload progress, set session.upload_progress.enabled=0 — the advisory recommends it even after patching.

What the two vulnerabilities are

Both are rated Critical, CVSS v4 9.2. They live in how the PHP engine manages per-request memory, which is why an application with otherwise sound code can still be exposed.

CVEAdvisoryRoot cause (per the advisory)
CVE-2026-109618
“RCE via OOM corruption”
GHSA-w8hw-w9fx-j79wPHP uses longjmp() to escape on out-of-memory errors. If that happens while request data is only partially initialised, request cleanup can then treat the invalid data as valid. The fix replaces this path with a delayed soft OOM and an immediate hard OOM via _exit().
CVE-2026-109619
“RCE via stale global state”
GHSA-6wmf-4rv5-p3rpRequest-owned state can survive in a persistent worker after its memory is freed. A later request on the same worker can use that stale state before normal initialisation resets it — a use-after-free. Confirmed impact: unauthenticated heap use-after-free, memory disclosure and RCE.

Affected and fixed versions

BranchVulnerableFixed in
8.5before 8.5.128.5.12
8.4before 8.4.278.4.27
8.3before 8.3.368.3.36
8.2before 8.2.358.2.35 (8.2 reaches end of life on 31 December 2026)
8.1 and olderNo longer updated by the PHP projectUpgrade to a supported branch

The 24 September 2026 releases (8.5.11, 8.4.26, 8.3.35, 8.2.34) are not fixed. “We patched last month” does not cover this.

Which server setups are exposed

SAPICVE-2026-109618 (OOM)CVE-2026-109619 (stale state)
Apache mod_phpConfirmed; full RCE validated over HTTPAffected; fatal-error backtrace path has confirmed RCE
PHP-FPMSusceptible by source analysis; no complete exploit demonstratedAffected; session lifecycle path validated
FrankenPHPLikely susceptibleNo early consumer found in default config — “does not make this environment immune”
LSAPILikely susceptible, unvalidatedConditional candidate

The advisories don't discuss CGI or CLI. Treat every PHP binary as in scope and patch it; don't rely on the absence of a row.

Step 1 — Find every PHP runtime you run

The usual miss is not the main web server — it is the forgotten one: a legacy VM, a cron box, a container base image pinned months ago, a WordPress marketing site on shared hosting. Inventory first, then patch.

# Hosts: CLI and FPM can be different binaries/versions
php -v
php-fpm -v 2>/dev/null || ls /usr/sbin/php-fpm* 2>/dev/null

# Apache with mod_php loaded?
apachectl -M 2>/dev/null | grep -i php

# Containers currently running
for c in $(docker ps -q); do
  printf '%s: ' "$(docker inspect -f '{{.Name}} {{.Config.Image}}' "$c")"
  docker exec "$c" php -r 'echo PHP_VERSION, PHP_EOL;' 2>/dev/null || echo 'no php'
done

# Exit non-zero if this PHP is older than the fixed release for its branch
php -r '$f=["8.2"=>"8.2.35","8.3"=>"8.3.36","8.4"=>"8.4.27","8.5"=>"8.5.12"];
$b=PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;
if(!isset($f[$b])){echo PHP_VERSION," unsupported branch\n";exit(2);}
$ok=version_compare(PHP_VERSION,$f[$b],">=");
echo PHP_VERSION,$ok?" OK\n":" VULNERABLE\n"; exit($ok?0:1);'

Check what the web server actually executes, not just the CLI on the host — a temporary phpversion() endpoint behind authentication, or the version in your FPM pool's startup log, tells you what serves traffic.

Distribution packages

Debian, Ubuntu, RHEL and similar often backport security fixes without moving to the upstream version number. If you install PHP from your distribution, check that distribution's own security tracker for CVE-2026-109618 and CVE-2026-109619 rather than comparing version strings.

Step 2 — Patch, then restart every worker

Installing the package is half the job. Both bugs live in long-running workers, so old worker processes keep running vulnerable code until they are restarted.

# Package-managed hosts (example: Debian/Ubuntu with FPM)
sudo apt-get update && sudo apt-get install --only-upgrade 'php8.3*'
sudo systemctl restart php8.3-fpm

# mod_php: restart Apache itself
sudo systemctl restart apache2   # or httpd

# Containers: rebuild from a fixed base, don't patch in place
docker build --pull -t my-app:patched .
# Kubernetes: roll the Deployment so every pod is replaced
kubectl rollout restart deployment/my-php-app

In containers, a floating tag like php:8.3-fpm only helps after --pull and a rebuild. Images pinned to a patch version or digest have to be bumped explicitly.

Step 3 — If you cannot patch today: partial mitigations

Read this first

Both advisories are explicit that updating is the only complete mitigation. Everything below reduces exposure; none of it closes the issue. Apply these settings administratively (php.ini, or php_admin_value in the FPM pool), not via ini_set() in application code, and restart every worker afterwards.

For CVE-2026-109619 (stale global state)

; php.ini — disable session upload progress (on by default; works without session_start())
session.upload_progress.enabled = 0

; PHP 8.5 only — the confirmed RCE path needs fatal error backtraces
fatal_error_backtraces = 0

; php-fpm pool (www.conf) — no worker reuse; costly, use only if performance allows
pm.max_requests = 1

The advisory also recommends removing attacker-reachable fatal errors, keeping diagnostics out of pre-initialisation INI settings, and avoiding predictable “Maximum execution time exceeded” interruptions. It warns that changing error_reporting, using @, or turning off display_errors / log_errors does not protect you.

For CVE-2026-109618 (OOM corruption)

The trigger is driving a request into out-of-memory while staying inside PHP's apparent limits. The advisory lists multipart parsing paths such as parts with unlimited headers, empty file controls (which don't count toward max_file_uploads), missing content lengths and deeply nested field names.

; php.ini — the advisory's primary configuration mitigation (not complete)
memory_limit = -1
; ...then confirm no pool, .user.ini, directory or app setting restores a finite limit

; keep request input conservative — size these to what your app needs
post_max_size = 8M
max_input_nesting_level = 16
max_input_vars = 1000
max_file_uploads = 20
  • memory_limit=-1 removes PHP's own ceiling. The advisory offers a container/cgroup memory.max limit as an alternative boundary, with limitations. It warns not to use RLIMIT_AS or RLIMIT_DATA, because they can trigger the vulnerable longjmp() path.
  • Enforce the decoded request size at the frontend (nginx, Apache, load balancer), not only in PHP.
  • If the application doesn't accept multipart form data, block it before it reaches PHP. A JSON-only API loses nothing.
# nginx — JSON-only API in front of PHP-FPM: reject multipart bodies
location ~ \.php$ {
    if ($content_type ~* "multipart/form-data") { return 415; }
    client_max_body_size 1m;
    # ... existing fastcgi_pass config
}

Step 4 — Verify

  1. Re-run the Step 1 inventory and require every runtime to report OK — including the ones that serve traffic, not just the host CLI.
  2. Check worker age. Every FPM or Apache worker should have started after the patch: ps -o pid,lstart,cmd -C php-fpm8.3 (adjust the process name).
  3. Confirm mitigations took effect where you applied them: php -i | grep -E 'session.upload_progress.enabled|memory_limit|fatal_error_backtraces' — and from a request served by the web SAPI, since pool and directory settings can differ from the CLI.
  4. Gate it. Put the version check from Step 1 into your image build or deploy script so a vulnerable PHP fails the build instead of shipping.

Step 5 — Look back, and rotate what a worker could see

CVE-2026-109619's confirmed impact includes memory disclosure. A PHP worker's memory can hold whatever recent requests touched: session data, database credentials, API keys, tokens. The advisories publish no indicators of compromise, so the practical signals are the conditions the bugs depend on:

  • Bursts of Allowed memory size of … bytes exhausted or Maximum execution time … exceeded errors, especially from unauthenticated or multipart requests.
  • Workers crashing — for example FPM log lines reporting a child that exited on signal 11 (SIGSEGV).
  • Unexpected processes spawned by the PHP or web-server user, and unexpected outbound connections.

The CVE-2026-109619 advisory suggests temporarily enabling log_errors=On and error_reporting=E_ALL in startup configuration during an isolated audit — to find issues, not as protection. If anything looks wrong, or you cannot rule it out on an internet-facing mod_php host, rotate the secrets that host's PHP workers held and treat it as an incident.

Related releases in the same week

  • 23 October — session upload progress (CVE-2026-115560). An informational advisory for a related issue that the PHP project says “can only occur under an unusual combination of configuration and application code”. It recommends session.upload_progress.enabled=0 until the next regular bugfix release, with a full advisory to follow.
  • 26 October — Windows builds 8.5.12-1, 8.4.27-1, 8.3.36-1, 8.2.35-1. These bundle Brotli 1.2.1, fixing CVE-2026-111497, an out-of-bounds write in Brotli decompression. On Windows it is reachable through the cURL extension only when automatic HTTP response decompression is enabled, which is off by default.

The short version

Patch to 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, restart every worker, and prove it with a version check against what actually serves traffic. If you can't patch today, disable session upload progress, harden request limits, block multipart where you don't need it — and schedule the patch anyway, because the PHP project is clear that nothing else fully closes this.

The lasting lesson is the inventory. If you already know every PHP runtime you run, and can rebuild every image from a fresh base in one command, a critical runtime CVE becomes a routine change instead of an emergency. More on that in Is Your Organisation Ready for the Next Critical CVE?

Official sources

Need a second pair of hands on patch rollout?

I help teams build the runtime inventory, rebuild-from-base pipelines and deploy-time version gates that turn a critical CVE into a routine afternoon.

Get in touch