On October 21, 2026 PHP shipped an emergency release for two unauthenticated RCE bugs, nicknamed "PHP2Shell": CVE-2026-109618 (memory corruption during out-of-memory handling) and CVE-2026-109619 (a use-after-free on state left behind between requests). Both are CVSS v4 9.2. They need no login, no app bug, and in the worst case no application code at all - just a crafted HTTP request to a PHP worker. Reported to reach PHP 5, 7 and 8. Fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35. The details leaked ahead of the embargo, Cloudflare saw exploitation the same day, and roughly 110M of 155M internet-facing PHP installs are on end-of-life branches that get no official fix. Patch, restart every worker, rebuild every image.
Introduction
PHP runs a large chunk of the web - the CMS behind your dentist's website, half the e-commerce checkouts you have ever used, and a long tail of internal tools nobody has touched since 2017. That ubiquity is exactly why a bug in PHP core is not a vendor problem. It is an internet problem.
On October 21, 2026 the PHP project cut an unscheduled security release for two vulnerabilities that the community had already started calling PHP2Shell: CVE-2026-109618 and CVE-2026-109619. Both are rated CVSS v4.0 9.2 (Critical). Both allow unauthenticated remote code execution with the privileges of the web worker. Neither requires a vulnerable application on top - the demonstrated attacks live entirely inside the PHP runtime that parses the request.
This is not a plugin bug. It is not a framework bug. It is the interpreter itself, in the code path that handles a request before your code ever runs. And the disclosure did not go to plan - it leaked a day early, a PoC started circulating, and WAF vendors were reporting attack traffic within hours of the patch.
This post is the anatomy: what actually broke, why it is so dangerous, what the real-world timeline looked like, and - the part that matters for you - exactly how to tell if you are exposed, how to patch, and how to harden if you cannot patch yet.
This post deliberately contains no working exploit, trigger payload, or step-by-step reproduction. PHP2Shell is an unauthenticated RCE that affects essentially the entire PHP install base, it is being exploited in the wild right now, and a fully reliable public exploit is still coming together. Publishing a weaponized reproduction would hand mass attackers a loaded gun pointed at the internet. Everything below is defensive: understand the bug class, find your exposure, patch, verify. That is the responsible version of "reproduce it" - and it is the version that actually helps you.
The disclosure that leaked
Before the mechanics, the drama - because it shaped how fast everyone had to move.
The flaws were on a coordinated-disclosure embargo with a planned announcement. On October 20, a now-removed "Tell HN" post named two reserved PHP RCE CVE IDs, a claimed CVSS 10.0, and a release date, citing an unnamed "reliable source." Screenshots spread across X and Mastodon. watchTowr publicly confirmed the leak was real the same day. With the cat out of the bag, the PHP security team posted an advance notice and then released the fixes early, around 14:00 UTC on October 21, rather than let unpatched details sit in the open.
Two things to note. First, the CVSS 10.0 figure that went viral was wrong - the PHP Foundation scored both at 9.2. The difference is not cosmetic: the v4 vector AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H says attack complexity is high and there is a passive precondition. These are not point-and-click trivial. They are, however, unauthenticated and devastating when they land.
Second, exploitation reporting ran ahead of the patch. Tenable relayed that Amazon threat intelligence observed an exploitation attempt on October 17 - four days before disclosure. Cloudflare and watchTowr honeypots reported attack attempts around release day. Patch windows do not get tighter than "the exploit was seen before the fix existed."
What actually broke
Both bugs share a theme: PHP's request lifecycle assumes requests start cleanly, run, and tear down cleanly. These flaws attack the moments when that assumption fails - when a request dies half-initialized, or when a worker is reused and carries a ghost of the previous request.
CVE-2026-109618 - memory corruption during OOM handling
When a memory allocation fails, PHP abandons the request abruptly. The problem: if the request's internal data was only partly initialized when the allocation failed, the cleanup routine runs anyway - over data that is not in a valid state. Operating on half-constructed internal structures is the classic recipe for memory corruption, and from there, code execution.
The nasty part is how you reach it. A crafted multipart request can exhaust memory even when its declared size looks within PHP's configured limits, and any application operation that sizes an allocation from attacker input can push the process over the edge. Under Apache mod_php, cleanup runs even when request startup fails - so the demonstrated attack lands before application code executes at all, under default input and memory limits, including multi-worker prefork setups. PHP-FPM terminates a worker when startup fails, which blocks that specific sequence; source analysis suggests FPM is still reachable after a successful startup, but no complete FPM exploit for this CVE was reported at release.
CVE-2026-109619 - use-after-free across requests
This one abuses worker reuse. PHP workers handle many requests in their lifetime. Internal state is supposed to be reset between them. Here, a pointer to memory freed at the end of an earlier request survives, and a later request on the same reused worker can touch that stale value before initialization clears it.
What keeps the stale state alive? Fatal errors - memory exhaustion, execution timeouts - that interrupt the normal cleanup, plus shutdown diagnostics that repopulate error state after it was reset. Two confirmed paths:
- mod_php on PHP 8.5: via the fatal-error backtrace feature.
fatal_error_backtracesis on by default, and a later initialization diagnostic reaches stale backtrace state. - mod_php and FPM: via the session lifecycle. Session upload progress is enabled by default and can consume stale session or decoder state before normal initialization. You do not need to call
session_start(). On FPM it requires an app operation that leaves state behind, then a fatal error, then another request on the same worker - multiple workers and worker recycling lower the odds per request but do not close the hole.
Read those two paragraphs again and notice what is missing: a login, a vulnerable plugin, a misconfiguration you made. The default configuration of a stock PHP web server is the vulnerable configuration. That is what makes this a fleet-wide emergency rather than a "patch it next sprint" advisory.
Why it is so bad
- Unauthenticated. No credentials, no session, no CSRF token. A request is enough.
- No application bug required. For the mod_php OOM path, the attack completes before your code runs. Your perfectly-audited app does not save you.
- Memory disclosure, not just RCE. Researchers also demonstrated unauthenticated disclosure of PHP worker memory - which can leak passwords, auth tokens, and API keys straight out of a live worker. Even where full RCE is unreliable, an info-leak that spills your secrets is plenty bad.
- The blast radius is the entire web. watchTowr reported the flaws reach PHP 5, 7 and 8 in their entirety (the pre-8.2 scope is not vendor-confirmed, but that is the claim). Qualys inventoried roughly 155M internet-facing PHP installations with a known version - about 44M on supported branches (8.2 to 8.5) and about 110M on end-of-life versions that will never get an official fix.
Every web-facing SAPI is in scope. Priority order:
| SAPI / deployment | Exposure | Priority |
|---|---|---|
Apache mod_php | OOM path lands pre-app-code; vendor's highest-risk target | Patch first |
php-cgi | Web-facing, in scope | High |
| FrankenPHP | Web-facing, in scope | High |
| LiteSpeed LSAPI | Web-facing, in scope | High |
| PHP-FPM | Startup-failure path blocked; session path still reachable | High |
| CLI | Not a web request surface | Low |
Affected and fixed versions
| Branch | Status | Fixed version |
|---|---|---|
| 8.5 | Affected before 8.5.12 | 8.5.12 |
| 8.4 | Affected before 8.4.27 | 8.4.27 |
| 8.3 | Affected before 8.3.36 | 8.3.36 |
| 8.2 | Affected before 8.2.35 | 8.2.35 |
| 8.1, 8.0, 7.4 | End-of-life upstream | Limited source backports only |
| 7.x and 5.x (older) | Reported affected | No upstream fix - distro backport or upgrade |
Do not trust the version number alone. Linux distributions backport security fixes without bumping the upstream version, so a "8.2.28" from your distro may already contain the fix - or may not. The only reliable signal is the vendor changelog mentioning CVE-2026-109618 and CVE-2026-109619. And php -v reports the CLI binary, which is frequently not the binary serving your web requests. Check the runtime that actually handles traffic (mod_php module, FPM pool, container image), not your shell.
A safe lab: find your exposure and verify your fix
The right way to "reproduce" this on your own Docker/OrbStack setup is not to fire an exploit - it is to build a small, isolated, network-off lab that answers three defensive questions: Am I running an affected build? Did my mitigation actually apply? Did my patch actually land? No payloads, nothing that leaves the box.
1. What version is this image actually running?
Spin a throwaway container with no network, and read the runtime version:
# --network none: this container can't reach anything, and nothing reaches it
docker run --rm --network none php:8.3-cli php -v
docker run --rm --network none php:8.3-fpm php -v # the SAPI that serves requests
2. Am I patched? (a version-compare you can trust)
php -v gives you a string. Turn it into a verdict with branch-aware comparison. Drop this in the container and run it - it strips distro suffixes like ~deb12u1 and -dev, and knows each branch's fix floor:
<?php
// First FIXED patch version per supported branch for PHP2Shell.
$fixed = ['8.5'=>'8.5.12','8.4'=>'8.4.27','8.3'=>'8.3.36','8.2'=>'8.2.35'];
function verdict(string $v, array $fixed): string {
$v = preg_replace('/[^0-9.].*$/', '', $v); // strip -dev, ~deb12u1, etc.
[$maj, $min] = array_pad(explode('.', $v), 2, '0');
$branch = "$maj.$min";
if (version_compare($v, '8.2.0', '<')) return "$v: EOL branch - distro backport or upgrade";
if (!isset($fixed[$branch])) return "$v: unknown branch - check vendor advisory";
return version_compare($v, $fixed[$branch], '>=')
? "$v: PATCHED (>= {$fixed[$branch]})"
: "$v: VULNERABLE (fix is {$fixed[$branch]})";
}
echo verdict(PHP_VERSION, $fixed), "\n";
docker run --rm --network none -v "$PWD":/w php:8.3-cli php /w/check-php2shell.php
# e.g. "8.3.35: VULNERABLE (fix is 8.3.36)"
Remember the distro caveat: a VULNERABLE verdict on a distro build that has a backported fix is a false alarm - confirm against the changelog. On a plain upstream php:* image, the verdict is accurate.
3. Did my mitigation apply to the running config?
Mitigations set in the wrong place (app-level ini_set(), .user.ini) apply too late to matter. Verify from the runtime's own view:
docker run --rm --network none php:8.3-fpm \
php -i | grep -Ei 'session.upload_progress|memory_limit|fatal_error_backtraces'
You want session.upload_progress.enabled => Off, memory_limit => -1 (if you took that route), and on 8.5, fatal_error_backtraces => Off.
4. Are you leaking your version to the whole internet?
Not a fix, just hygiene - but attackers scan on it. Check whether your server advertises PHP:
curl -sI https://your-own-domain.example/ | grep -iE 'x-powered-by|server'
If you see X-Powered-By: PHP/8.3.x, set expose_php = Off. It will not stop a determined attacker, but it drops you out of the easy mass-scan bucket.
Patch - this is the only real fix
Everything else buys time. The fix is the version bump.
- Update PHP to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (or your distro's build whose changelog names both CVEs). On EOL branches, apply the official source backport (7.4 / 8.0 / 8.1) or upgrade the branch.
- Restart every PHP service.
systemctl restart php8.3-fpm, reload Apache for mod_php, bounce LiteSpeed. Running processes keep the vulnerable code in memory until restarted - a patched package with un-restarted workers is still exploitable. - Rebuild and redeploy every container image. Patching a running container is throwaway work; the next deploy from the old image reintroduces the bug. Rebuild from a fixed base, including parallel/secondary PHP branches you forgot you had.
- Find all of it. cPanel/Plesk multi-PHP, that one legacy VM, the sidecar container, the vendored binary in a custom path. Qualys reported ~110M of ~155M installs are on EOL branches - the forgotten ones are the ones that get popped.
- Rescan and verify with the lab above, then rotate anything that could have leaked if you were exposed before patching (see Detection).
If you cannot patch in the next hour
These are the official PHP temporary measures. They reduce exposure and do not make an unpatched install safe - treat them as a bridge, with a named owner and a deadline.
| CVE | Mitigation | Set it where |
|---|---|---|
| 109618 (OOM) | memory_limit = -1, enforced so apps can't override | php_admin_value / FPM pool / Apache conf |
| 109618 | post_max_size = 8M (or 32M if uploads needed), upload_max_filesize below it | server/pool config |
| 109618 | Lower max_input_nesting_level (default 64) and max_file_uploads; block multipart if unused | server/pool config + proxy |
| 109619 (session) | session.upload_progress.enabled = 0 | php.ini / FPM pool / server conf (not ini_set, not .user.ini) |
| 109619 (8.5) | fatal_error_backtraces = 0 | admin config, then restart workers |
| 109619 (FPM) | pm.max_requests = 1 if you can eat the perf cost (kills worker reuse) | FPM pool |
Restart all workers after any change. Two traps to avoid: do not use RLIMIT_AS / RLIMIT_DATA (they can trigger the vulnerable OOM path), and be wary of cgroup/container memory limits (enforcement can kill the master and take every worker with it). And note what does not help: silencing diagnostics with error_reporting, display_errors, log_errors, or @ does nothing to the vulnerability - the absence of errors in your logs is not evidence you are safe.
How the ecosystem responded
The platform layer moved faster than most individual servers could:
- Cloudflare reported attack traffic targeting these flaws - including attempts to evade its protections - and pushed WAF coverage. If you sit behind Cloudflare, you got a head start, not immunity: WAF rules reduce opportunistic scanning but are not a substitute for patching the origin.
- Linux distributions and LTS vendors received patches in advance of public disclosure and shipped on or near release day (for example, Ubuntu's
USN-8948-1, a userspace-only update with no reboot required). The volunteer-run Remi repository notably did not have updates on day one - PHP's own advisory told Remi users to switch to distro packages or build from source. - Aikido published free backported PHP images on a public registry covering PHP 7.3 through 8.5, keeping existing version tags so you can swap the base image without a version jump - a genuinely useful stopgap for teams stuck on an old branch.
- PHP's security team published minimal source backports for 7.4 / 8.0 / 8.1 for teams that build from source.
Detection and the morning-after
There is no clean, PHP-level indicator of compromise published yet, so detection is mostly inference:
- Crashes and segfaults. The reliable public exploits so far are narrow - they work only for specific OS + PHP version + web server combinations. Unreliable exploitation against the wrong target tends to crash workers. A spike in PHP-FPM/Apache segfaults or worker restarts around and after October 17 is a red flag worth hunting in your logs.
- Odd multipart traffic. Both primary paths involve multipart/upload handling. Unusual volumes of malformed multipart requests, or requests engineered to spike memory, are worth alerting on at the proxy.
- Assume secret exposure if you were unpatched and exposed. Because the memory-disclosure variant can leak tokens and keys from worker memory, treat credentials handled by any exposed, unpatched worker as potentially burned: rotate app secrets, DB creds, and API keys, and review access logs for the window before you patched.
As for "which big sites got hit" - resist the urge to name names. As of this writing there are confirmed exploitation attempts (Cloudflare, watchTowr honeypots, Patchstack-reported incidents, the pre-disclosure Amazon observation) and some government and organizational sites were taken offline precautionarily, but no widespread confirmed compromise of a named major target has been publicly detailed. This is opportunistic mass-scanning against the long tail far more than a targeted hit on marquee names - which, given 110M EOL installs, is cold comfort.
Conclusion
PHP2Shell is the kind of bug that makes the request lifecycle itself the attack surface. CVE-2026-109618 corrupts memory when an allocation fails mid-request; CVE-2026-109619 reaches back into a previous request's freed state on a reused worker. Neither needs your login or your bug - just a crafted request and a default PHP configuration, which is to say, almost everyone's.
The complexity is genuinely high, the fully-reliable exploit is still coming together, and that is exactly the window you are in right now: enough time to patch, not enough to procrastinate. Bump the version, restart every worker, rebuild every image, find the installs you forgot about, and rotate what might have leaked. The 110 million end-of-life installs are not going to patch themselves - make sure yours are not in that pile.
PHP2Shell (CVE-2026-109618 / CVE-2026-109619, both CVSS 9.2) is an unauthenticated RCE in PHP core that works against default configs across PHP 5, 7 and 8. The version number alone lies - verify against the changelog for both CVE IDs, and check the runtime serving traffic, not your CLI. Patch to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, restart every worker, rebuild every image, and if you were exposed before patching, rotate your secrets. Mitigations buy hours, not safety.
References
- PHP2Shell CVE-2026-109618 & CVE-2026-109619: Critical Unauthenticated RCE - Rapid7
- Critical Security Release for Two Vulnerabilities - Qualys
- PHP CVE-2026-109618 FAQ - watchTowr
- php2shell (CVE-2026-109618, CVE-2026-109619) FAQ - Tenable
- PHP2Shell: How to fix PHP RCE with patches and free images - Aikido
- CVE-2026-109618 & CVE-2026-109619 Analysis - SOC Prime
- PHP2Shell: Affected Versions & Fix - SOCRadar
- PHP Core RCEs Leak Ahead of Embargo - Threat Landscape
- 800 Systems affected: PHP Core RCE patched October 21 - Phoenix Security
- Critical RCE vulnerability in PHP: find impacted assets - runZero