php2shell: the php core rce, dissected
PHP shipped an emergency release on 21 October 2026: CVE-2026-109618 and CVE-2026-109619, fixed in 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Unauthenticated, remote, no user interaction, CVSS v4 9.2. The community label is “php2shell”.
I want to be precise about what follows, because the vendor analysis of this one disagrees with itself in ways that change what you should do.
I run a small threat intel pipeline for this site, so I checked its numbers before writing anything:
| Measurement | Result |
|---|---|
| CISA KEV catalog (version 2026.10.21, 1754 entries) | Neither CVE listed |
| FIRST EPSS | No score published for either yet |
| Vendor and community chatter tracked since 18 Oct | 459 mentions for -109618, 322 for -109619 |
| Distinct GitHub repos carrying a PoC, scanner or mitigation kit | 711 |
| Tier-1 advisories picked up (CERT-EU, CERT-FR, CISA, Fortinet, Cisco, Wordfence) | 8 per CVE |
711 distinct repositories in four days is the number that stuck with me. That is what an unauthenticated RCE in the PHP runtime looks like from the outside.
the two bugs are not the same bug
The advisories split into two CVEs, and the split matters for triage, because they have different triggers and different mitigations.
cve-2026-109618, rce via oom corruption
GHSA-w8hw-w9fx-j79w. PHP’s own words: “out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid.”
The mechanism underneath is longjmp(). Older PHP releases use longjmp() to abandon request handling after an out-of-memory event, so an allocation failure can bypass the expected accounting and leave inconsistent state that request shutdown then reads or branches on. The fix replaces the bailout with a delayed, graceful soft OOM at the next VM safe point plus an immediate hard OOM via _exit(), and resets globals that may be reached during startup. Upstream commit 3584a98bda, “Avoid state corruption via bailout”, by Arnaud Le Blanc, committed 2026-10-21.
cve-2026-109619, rce via stale global state
GHSA-6wmf-4rv5-p3rp. “Request-owned internal state can survive after its backing memory has been freed. A later request handled by the same worker may use that stale state before normal initialization resets it.”
That is a use-after-free with a twist: the producer and the consumer are two different HTTP requests served by the same worker, so the primitive crosses the request boundary. Two paths are confirmed. One is session.upload_progress, where the fix adds a PS(in_rfc1867_update) guard around php_session_rfc1867_update() in ext/session/session.c. The other is fatal_error_backtraces on PHP 8.5.
Both carry CVSS AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N and the CWE set 416, 755, 824, 1325, 1419. Brandefense notes NVD published 9.8 on CVSS 3.1, a different scoring system rather than a different judgment. The PHP Foundation notes the first release went out with a wrong release time and a wrong score of 10.0, the real one being 9.2.
PHP’s risk statement is the part to internalize: “all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs.”
Before application code runs. The trigger lives in the runtime’s request parsing path, which means the usual defensive layers, an auth gateway, a WAF rule, an app level middleware, all sit downstream of the bug.
the exploitability claims do not agree
This is the part I would push a vendor on. Three credible readings of the same evidence:
- Qualys: one request per attempt for -109618 on mod_php, two requests on the same worker for -109619, and “exploitation depends on the allocator layout, build, and worker scheduling”.
- Sansec, the sharpest skeptic: “The public exploits do not work against a default PHP installation”, “Nobody has shown such a request for PHP2Shell”, “turning off session upload progress alone is not enough”. Every known trigger is a
multipart/form-datarequest. - Unit 42, the outlier: attackers can “reliably achieve RCE using out-of-memory (OOM) triggers”, with no method shown.
Take them together and the defensible reading is that exploitability is environment specific. It depends on the allocator layout, the build flags, the SAPI, the configured limits and worker scheduling. Which means the question “is this host vulnerable?” is not answerable from the version string alone, and anyone selling you a version range scanner is selling you the easy half of the problem.
I would not repeat the 22.6 million figure from Unit 42’s Xpanse as a count of exploitable hosts. It is exposed instances that potentially match the affected range, an upper bound on inventory, not a measurement of exploitability.
what we actually know about attacks
Attacks, not breaches. That distinction is doing a lot of work right now.
- Cloudflare had protections rolling out about nine hours before public disclosure and observed “attack traffic targeting our customers”, with “attackers mutating the attack payload to try to bypass our protections”. Default WAF protection for all tiers from 21 October 03:02 UTC.
- Amazon Threat Intelligence reported an exploitation attempt dated 17 October 2026, before public disclosure.
- watchTowr observed exploitation through its honeypot network as of 21 October.
- Hadrian reported exploitation at scale, mostly through the session upload progress path, per Brandefense.
- Huntress saw nothing on partner systems as of 21 October 16:30 ET, and has not reproduced a PoC.
- Sophos found no evidence of exploitation, took its own MyUTM portal offline on 21 October, and recommends reviewing exposed systems from 1 August 2026 onward.
- CISA had not observed exploitation, and neither CVE is in KEV.
Meanwhile a European government and a handful of Dutch sites, among them Tweakers, the Dutch House of Representatives, NCSC and Security.NL, took themselves offline as a precaution. The PHP Foundation says they “did not identify any functional exploits at the time of release”. So the honest summary: loud, early and mostly unsuccessful attempts against a very large attack surface, with the success rate unknown.
what to do
Patching is the only complete fix, and PHP says so plainly. The interim measures are per path, and PHP published them as something that “reduce[s] exposure to specific attack paths but do[es] not make an installation safe”.
for cve-2026-109618
memory_limit=-1, set where applications cannot override it (php_admin_valuein the FPM pool or the Apache config), and check that no pool, directory or app setting puts a finite limit back. PHP’s caveat: an underlying allocation failure can still reach the vulnerable path.- Low
post_max_size, 8M if you do not need uploads and 32M if you do. Keepupload_max_filesizebelow it. - Enforce limits at the web server or proxy for decoded request size, input field count, multipart part count, header count and nesting depth, and cover requests without a usable Content-Length.
- Use a container or cgroup limit as the availability boundary, with a policy that kills only the affected worker. Do not substitute
RLIMIT_ASorRLIMIT_DATA, which can cause the vulnerable allocation failure.
for cve-2026-109619
session.upload_progress.enabled=0and restart every worker. Apply it inphp.ini, the FPM pool or server config, so it lands before request parsing.ini_set()and FPM.user.inido not work here.- On PHP 8.5,
fatal_error_backtraces=0and restart every worker. That blocks the backtrace path only. - Remove per request invalid or deprecated settings that emit diagnostics before initialization. Hiding diagnostics with
error_reporting,display_errorsor@does not help. - Fix attacker reachable fatal errors and execution timeouts, including callbacks, upload or import processing and cleanup.
hunting
Hunting is behavioral, because no indicators of compromise have been confirmed and no public YARA or Sigma rules exist. The signal Huntress recommends is a web server process spawning a child process. Parents php-fpm, php-cgi, httpd, apache2; children sh, bash, cmd.exe, powershell.exe, python, perl, curl, wget, nc, certutil.exe; command lines with /dev/tcp/, fsockopen, mkfifo; recon calls whoami, id, uname -a, ipconfig.
Sophos adds the request side shape: multipart POSTs to endpoints that do not normally accept uploads, PHP_SESSION_UPLOAD_PROGRESS in the body, memory exhaustion errors and worker segfaults, new or modified PHP files, and outbound connections from PHP to unexpected destinations.
One trap. In process protections do not necessarily help. Sophos notes that RASP and application embedded WAF agents may not prevent exploitation, and Sansec makes the same point from the other end: the RCE and memory disclosure paths run inside PHP, before your code, so you cannot fix this from your store code.
implications
the bailout design is the real bug
A longjmp() out of an allocator failure path is a memory safety violation in the language runtime itself. It unwinds past the point where the allocator’s accounting is consistent, and the shutdown path then trusts state that was never finished. Wikipedia’s account of the 2024 audit says the finding was never fixed because the development team could not reach consensus on how to fix it. Two years later it shipped in an emergency release next to a second bug from the same family. The individual PoCs are the news. The known finding sitting on a design disagreement for two years is the systemic story.
pre-parser reachability inverts the usual risk model
Both CVEs fire in the runtime’s request handling, before application code. For a decade the industry default has been that unauthenticated means the application can decide, which holds for logic bugs and stops being true for runtime memory corruption. Your auth gateway does not protect the worker that parses the request before the gateway’s request ever reaches your app.
version is not a risk verdict
If the vendor analysis is right that exploitability depends on allocator layout, build, SAPI and limits, then the operational question for a large fleet is not which version but which version, built how, served how, with what limits. That is an inventory problem before it is a patching problem, and the people most exposed are the ones running PHP they did not know was there. The 8.1 problem compounds it. PHP is not shipping binaries for 8.1 and older, only minimal source backports for 7.4, 8.0 and 8.1, and the volunteer rpms.remirepo.net had no update that day.
do not confuse this with CVE-2024-4577
Different bug class, different platform, different shape. That one was argument injection in php-cgi on Windows, and it has been in CISA KEV since 12 June 2024 with known ransomware use. This pair is memory corruption in the PHP core, it is not Windows specific, and as of the catalog I checked today it is not in KEV. Same label turning up in search results, different bug. Worth stating plainly, because triage time is the scarce resource during a patch cycle.
the disclosure timeline is its own lesson
Advance notice went out on 20 October, the release was published on 21 October, and attack traffic was already visible days earlier. Cloudflare had protections live about nine hours before public disclosure. A PoC video was published in error, and the first release carried both the wrong time and the wrong score. Each of those is a small coordination failure, and their sum is that defenders got less warning than the CVE system implies.
sources
PHP security advisory · PHP 8.3.36 release announcement · GHSA-w8hw-w9fx-j79w · GHSA-6wmf-4rv5-p3rp · fix commit 3584a98bda · PHP Foundation statement · Qualys · Sansec · watchTowr FAQ · Unit 42 · Huntress · Sophos · Cloudflare · Google Cloud · ENISA joint statement · NCSC-NL operational info · CISA KEV catalog · FIRST EPSS