Two critical remote-code-execution flaws in PHP (CVE-2026-109618 and CVE-2026-109619) were patched on 21 October 2026. What we found: the public “php2shell” exploit targets the wrong bug, the real flaw is engine-wide, and the loud “the patch is already broken” claims do not hold up.
TL;DR
On 21 October 2026 the PHP project shipped emergency fixes for two critical, unauthenticated remote-code-execution flaws (CVSS 9.2), nicknamed “php2shell” after the leaked exploit. We tracked them from the pre-disclosure leak to the patch and tested every major claim in our own lab.
- The real bug is in PHP’s memory engine. The press and the leaked proof-of-concept blamed PHP’s session upload-progress feature. But that feature is only one way to reach it and the fix touches around 30 files.
- It is an out-of-memory corruption bug. When PHP runs out of memory at the wrong moment, it jumps out of a half-finished internal operation and then trusts the broken leftovers. That is the whole vulnerability.
- The public exploit does not work. We ran it against a default PHP build and got no crash and no code execution. Its “indicators” identify the tool, not the vulnerability.
- Apache mod_php is the confirmed target. On Apache mod_php an attacker reaches code execution with no application bug and default settings. On PHP-FPM, exploitation was not demonstrated.
- “The patch is already bypassed” is false. Every bypass claim we checked needs a separate application vulnerability or is only a crash. Patched PHP is safe from these two CVEs.
| PHP branch | Status | Fixed version |
| 8.5 | Vulnerable below 8.5.12 | 8.5.12 |
| 8.4 | Vulnerable below 8.4.27 | 8.4.27 |
| 8.3 | Vulnerable below 8.3.36 | 8.3.36 |
| 8.2 | Vulnerable below 8.2.35 | 8.2.35 |
| 8.1 and older (incl. 7.x, 5.x) | Vulnerable, end-of-life | No fix, upgrade to a supported branch |
Timeline
| Date (UTC) | What happened |
| 20 Oct 2026 | Technical details leak on Hacker News and X before the coordinated release. PHP posts an advance notice of an emergency fix for two critical RCEs. |
| 21 Oct, ~05:00 | We set up an isolated lab and run the circulating “php2shell” exploit. It does not produce a crash or code execution. |
| 21 Oct, 13:34 | GitHub Security Advisories published: GHSA-w8hw-w9fx-j79w (CVE-2026-109618) and GHSA-6wmf-4rv5-p3rp (CVE-2026-109619). |
| 21 Oct, ~13:35 | PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 released; MITRE publishes both CVE records. |
| 21 Oct, ~13:50 | We diff the patch: the fix is in PHP’s core memory engine. |
| 21 Oct, 17:42 | A maintainer-verified crash regression (php-src #24488) is filed against the brand-new releases. |
| 22 Oct | “The patch is already bypassed” claims spread online. We reproduce the regression and verify each bypass claim, none is a real patch bypass. |
What are CVE-2026-109618 and CVE-2026-109619?
Both bugs come down to one sentence: when PHP runs out of memory at the wrong moment, it leaves its own internal state half-finished, and then trusts it.
Every PHP request has a memory budget (memory_limit, 128 MB by default). For years, when a request blew that budget, PHP “bailed out” a hard jump (longjmp) that abandoned whatever it was doing mid-step. That is usually harmless. But in some spots PHP has only partly built an internal object when the jump fires. The half-built object is left in a broken state, and a later step reads it or frees it again. An attacker who can line the memory up just right turns those broken leftovers into attacker-controlled data and then into code execution.
The two CVEs are two faces of the same problem:
- CVE-2026-109618 (“RCE via OOM corruption”): the out-of-memory jump corrupts memory inside a single request.
- CVE-2026-109619 (“RCE via stale global state”): a reused worker keeps a pointer to a previous request’s memory after it was freed, and a later request acts on it.
The official fix is a single commit, and its own message says it plainly:
Avoid state corruption via bailout
- Replace bailout on OOM with UnwindExit
- Properly reset various globals that may be accessed during startup
Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)In plain terms: stop the hard jump on out-of-memory. Instead PHP now finishes to a safe point and raises a normal fatal error, and a second, higher limit triggers an immediate clean process exit. It also wipes the leftover state between requests. The fix is spread across about 30 core and extension files. The session upload-progress path is hardened too, but it is only one of many ways to reach the bug: the flaw can be triggered from anywhere PHP runs out of memory.
Does the public php2shell exploit work?
The leaked exploit everyone calls “php2shell” targets PHP’s session.upload_progress feature with a crafted file upload. We rebuilt it in an isolated lab (nginx in front of PHP-FPM, default settings) and fired it repeatedly, using both the plain version and the “armed” version with the exploit payload:
109618 status HTTP/1.1 200 OK command_in_body False
109619 status HTTP/1.1 200 OK command_in_body False
... (every run)Every request returned a clean 200 OK, with no crash and no code execution. The exploit author’s own notes admit the same: their lab never triggered the bug either.
The reason is simple arithmetic. The exploit sends a request of about 62 KB, but a default PHP gives each request 128 MB of memory. It never comes close to running out, so the out-of-memory bug it depends on never fires. On a normal server, this tool does nothing.
It does leave a recognizable fingerprint: a fixed User-Agent (php2shell-109618 / -109619), 149 repeated t[] form fields, and a session file named after an attacker-chosen ID. Useful for spotting someone running the tool, but those are indicators of the tool, not of a successful attack. We still ship detections for them, clearly labelled as such.
Who is affected?
Exposure depends on how PHP is hosted, and the official advisory is specific about it:
| Web server | Exposure |
| Apache mod_php | Unauthenticated RCE with no application bug, at default memory and input limits. Validated over HTTP. Apache runs its shutdown routine even when request startup already failed on out-of-memory, that is where the broken state gets used. |
| FrankenPHP | Likely the same as mod_php (also runs shutdown unconditionally). |
| PHP-FPM | Not demonstrated. The worker exits when request startup fails, which blocks the confirmed attack. Source analysis still flags it as susceptible. |
| LiteSpeed (LSAPI) | Models PHP-FPM; app-specific exploits likely but unvalidated. |
The attack works by forcing PHP to run out of memory during request parsing, before any application code runs. That is why “we don’t use that feature” or “our app is safe” offers little protection. And the usual size limits don’t reliably stop it: the advisory lists tricks that stay within post_max_size yet still exhaust memory, a multipart upload with no declared length, an unlimited number of part headers (tiny on the wire, large in memory), empty file fields, and deeply nested field names.
The practical takeaway: scope your exposure by web server first. Apache mod_php and FrankenPHP are the priority. Our own lab ran PHP-FPM, the hardest case to exploit, so a clean result there does not mean Apache mod_php is safe.
Is the patch already bypassed?
No. Within a day of the release, posts and two public repos claimed the patched versions were still exploitable. Customers understandably got nervous. We checked every claim against the patched source and in our lab. None is a bypass of these CVEs. Each one either needs a separate vulnerability in the application, or is just a crash.
| Claim | What it really is | What it needs | Bypasses the fix? |
| Session upload race | Old session-file poisoning | A file-include flaw in the app | No |
| RLIMIT / memory failure | A crash (denial of service) patched PHP exits cleanly | An unusual memory_limit=-1 + OS limit config | No |
| /proc/PID/fd include | A 2018-era local-file-include trick | A file-include flaw in the app | No |
var_destroy unserialize bug | A separate memory bug, unrelated to these CVEs | The app must call unserialize() on attacker data and have a rare gadget class | No |
| Handler mismatch | PHP object injection | A gadget class + an unusual config | No |
| Session-to-include | Session poisoning | A file-include flaw in the app | No |
The pattern is consistent: these are known techniques that require the target application to already be vulnerable. They are not flaws in the PHP patch.
One is worth a footnote. The var_destroy claim points at a separate weakness in PHP’s unserializer that the security patch did not change. But it only matters if an application already hands attacker-controlled data to unserialize() with an unusual destructor gadget, a situation that is usually game-over on its own. We could not reproduce it cleanly with a minimal test, and we did not run the weaponized exploit. It deserves a heads-up to the PHP team as its own bug, not a “the patch failed” headline.
Did the patch introduce a new bug?
There is one new issue in the release, but it is not a security bypass. The fix bundle accidentally introduced a separate bug (PHP tracks it as php-src #24488, status: Verified): a double-free in the SimpleXML extension’s garbage collector, caused by an unrelated bugfix that shipped in the same release.
We reproduced it on the official Docker images, with a clean before/after:
php:8.4.26 -> done (clean)
php:8.4.27 -> zend_mm_heap corrupted
-> free(): double free detected in tcache 2The important details:
- It is a crash (denial of service), with no code execution demonstrated.
- It only triggers from application code, specifically
get_object_vars(), an(array)cast, orjson_encode()on aSimpleXMLElementthat sits inside a reference cycle the garbage collector cleans up. It is not reachable pre-authentication. - PHP will fix it in the next point release (8.4.28 / 8.5.13).
This is almost certainly the “double-free regression” fueling the online chatter that “the fix isn’t fixed.” Even the researcher whose post customers forwarded to us said it clearly: “Neither finding establishes that the php2shell patches failed or that RCE is possible.”
How can exploitation be detected?
This vulnerability is hard to catch at the moment of the trigger. Forcing out-of-memory during request parsing spawns no new process, writes little to disk, and the crash itself is often invisible to runtime telemetry. And because the real bug is in PHP’s core, blocking the leaked tool’s fingerprint would miss any attacker who triggers the same flaw a different way.
The lesson for defenders: don’t anchor on the exploit’s trigger, watch for the code execution and the crashes that follow. Upwind’s runtime sensor sees the process a PHP worker launches and the lineage behind it, which is exactly where this attack becomes visible.
Indicators of Compromise
Fixed versions are in the summary table above. These are the signs of the tool and of the real attack. Treat the tool fingerprints as “someone is probing,” and the host-log and process signals as “this may be a real attempt.”
| Indicator | Where to look | What it means |
A multipart POST with a PHP_SESSION_UPLOAD_PROGRESS field and ~149 repeated t[] fields | Web / proxy logs | The leaked tool’s request shape |
sess_<attacker-chosen-id> files in the session directory (often /tmp) | Host / session save path | The tool wrote an attacker-named session file |
Allowed memory size ... exhausted, especially in Unknown on line 0, in bursts | PHP / PHP-FPM error logs | Out-of-memory during parsing/startup, the real trigger |
child N exited on signal 11 (SIGSEGV) repeating; nginx recv() failed (104: Connection reset by peer) ... fastcgi | PHP-FPM / nginx logs | Worker crashes consistent with exploitation grooming |
A PHP worker (php-fpm, apache2, httpd) spawning sh, curl, wget, nc or python | Runtime / EDR | Post-exploitation code execution, the highest-confidence sign |
What should security teams do now?
- Patch now. Upgrade to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 and rebuild your container images. This is the only complete fix.
- Prioritize Apache mod_php and FrankenPHP. Those are the confirmed, application-independent targets at default settings. PHP-FPM is lower risk but still patch it.
- Do not roll back to avoid the SimpleXML crash. Rolling back re-exposes the actual RCE. If a workload uses SimpleXML with
get_object_vars()orjson_encode()on cyclic data and you seezend_mm_heap corruptedcrashes, that is the #24488 regression, wait for the point release (8.4.28 / 8.5.13) or avoid that specific call pattern. - End-of-life PHP (8.1 and older) gets no fix upgrade. As a stopgap the official guidance is to set
memory_limit=-1administratively and restart workers, and to never useRLIMIT_AS/RLIMIT_DATA. Disablingsession.upload_progressand tightening input limits help but do not fully close it. - Ignore the “patch is bypassed” noise but fix your own app gaps. The circulating bypasses all need a local-file-include or an unsafe
unserialize()in your application. Those are worth fixing regardless of this CVE.
How can Upwind help?
Upwind gives you the two things this incident demanded: scope and runtime visibility. The sensor inventories which workloads run PHP and at which versions, and shows the web server in front of each one, so you can find and prioritize the Apache mod_php and FrankenPHP hosts that are actually exploitable, instead of guessing.
And because the only reliable signal for this flaw is what happens after exploitation, Upwind watches for exactly that: a PHP worker spawning a shell or a network tool, the process lineage behind it, and the crash bursts that precede a successful attempt.
To learn more about how Upwind finds exposed PHP workloads and detects post-exploitation activity, schedule a demo.
