PHP2Shell is the name for two critical vulnerabilities in the PHP interpreter itself, CVE-2026-109618 and CVE-2026-109619, both rated 9.2 on CVSS v4.0. Either one gives an unauthenticated attacker remote code execution as the web server user, and neither needs a bug in your application. PHP shipped fixed releases on 21 October 2026. Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35.
This one is worth reading carefully even if you have already patched, because the two bugs have different triggers, different reachable configurations and different partial workarounds, and most of the early coverage mixed them up.
What PHP2Shell is
Two separate memory-safety defects in the PHP engine, disclosed together under one name. Both were published as GitHub Security Advisories by the PHP project on 21 October 2026, and both carry the same severity and the same CVSS v4.0 vector.
| CVE | PHP advisory title | Advisory ID | What it gives an attacker |
|---|---|---|---|
| CVE-2026-109618 | RCE via OOM corruption | GHSA-w8hw-w9fx-j79w | Unauthenticated RCE as the web worker account |
| CVE-2026-109619 | RCE via stale global state | GHSA-6wmf-4rv5-p3rp | Unauthenticated use-after-free, memory disclosure and RCE |
Both are scored CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N, which reads as network reachable, no privileges, no user interaction, full confidentiality, integrity and availability impact on the affected host, with high attack complexity and attack requirements present. Both are filed against the same five weaknesses: CWE-416 use after free, CWE-755 improper handling of exceptional conditions, CWE-824 access of uninitialized pointer, CWE-1325 improperly controlled sequential memory allocation and CWE-1419 incorrect initialization of resource.
High attack complexity is the only comfort in that vector, and it is a thin one. The advisory for CVE-2026-109618 states that full remote code execution was validated over HTTP against mod_php using default PHP input and memory limits and a normal prefork configuration. Additional workers and unrelated traffic raise the number of attempts an attacker needs. They do not remove the condition.
How the PHP2Shell vulnerabilities work
Both bugs come from the same structural weakness: PHP can be interrupted part way through handling a request, and the code that runs afterwards assumes the interruption never happened.
CVE-2026-109618: an out-of-memory error that skips cleanup
When PHP hits an out-of-memory condition it calls longjmp() to leave the current context. In some contexts, request memory is reachable before it has been fully initialised. If an allocation fails at that point, PHP does not account for the early exit, and request memory is left in an invalid state. Request shutdown then reads or branches on that state, and that is the path to code execution.
What makes this reachable from ordinary traffic is that the allocation which fails does not have to be related to the memory that ends up corrupted. The advisory lists several ways a request can reach the memory limit while staying inside post_max_size:
- Multipart requests with no usable declared content length.
- An unlimited number of headers per multipart part.
- Empty file controls, which do not count towards
max_file_uploadsbut still create file metadata arrays. - Application-specific inputs that cause large allocations.
The fix replaces the immediate longjmp() with a delayed soft out-of-memory condition handled at the next safe point in the virtual machine, plus a hard out-of-memory path that calls _exit() at a higher threshold.
CVE-2026-109619: state from one request reused by the next
PHP keeps global state that a worker holds between requests, covering error, session, object and extension state. Some of that state can point at memory owned by a previous request. When a fatal error interrupts a request before its cleanup runs, or a diagnostic repopulates error state after shutdown has started, the request heap is freed while the global still references it. An operation that runs before normal request initialisation then uses the stale value. The result is a use-after-free that a crafted heap layout can turn into code execution.
Exploiting it needs two requests that land on the same worker: a producer that leaves the dangling state behind, and a consumer that reads it.
| Role | Confirmed cases from the PHP advisory |
|---|---|
| Producer | Parse and compile failures, uncaught errors, a failed require, "Maximum execution time exceeded", and out-of-memory conditions. Nonfatal diagnostics during later shutdown stages can also repopulate error state. |
| Consumer | INI diagnostics that run before request initialisation, including warnings from invalid or deprecated INI values even when they are never displayed or logged. Also session upload progress handling, which can run before ordinary session initialisation. |
On PHP 8.5 the advisory names one confirmed producer directly: with fatal_error_backtraces enabled, fatal error reporting stores request-owned backtrace state that is never reset on shutdown. The advisory describes that as the basis of the confirmed mod_php exploitation.
A second path runs through the session lifecycle. An out-of-memory condition during input processing can leave session and decoder globals pointing at freed memory, and the advisory records that this one was validated against both mod_php and PHP-FPM.
Which PHP versions are affected by PHP2Shell
The two advisories list affected and patched versions only for the branches PHP still supports. Treat the older branches as affected too, because the project went to the trouble of backporting the fix to them.
| Branch | Affected | Fixed release |
|---|---|---|
| 8.5 | below 8.5.12 | 8.5.12 |
| 8.4 | below 8.4.27 | 8.4.27 |
| 8.3 | below 8.3.36 | 8.3.36 |
| 8.2 | below 8.2.35 | 8.2.35 |
| 8.1, 8.0, 7.4 | end of life, no binaries | minimal source backports published by the PHP security team |
| 7.3 and older, 5.x | not covered by any advisory | nothing from the project |
On the question of how far back the defect reaches, the vendor is quiet and the researchers are not. Sansec dates the earliest affected code to 2012 and names PHP 5.4.0 as the first affected release, and watchTowr described the exposure as spanning PHP 5 through 8. Neither figure appears in the GitHub advisories, so treat the pre-7.4 range as credible reporting and not as a vendor statement. Either way the practical consequence is the same. If you are running anything below 8.2 you have no binary fix, and the answer is a distribution backport or an upgrade.
PHP 8.2 reaches end of life on 31 December 2026. If patching PHP2Shell means landing on 8.2.35, treat that as a stop on the way and not a destination.
Why mod_php is the worst case
Before you decide how urgent your own exposure is, check how PHP is wired into your web server. The same two bugs behave very differently depending on the answer.
| SAPI | Status in the PHP advisories |
|---|---|
| Apache mod_php | Worst case. Shutdown runs even when startup fails, so the condition is reachable before any application code runs. Full RCE validated over HTTP. |
| PHP-FPM | The child exits when startup fails, which blocks that path. A complete FPM exploit for CVE-2026-109618 was not demonstrated, though source analysis suggests susceptibility, and the session lifecycle path in CVE-2026-109619 was validated against FPM. |
| FrankenPHP | Likely susceptible on source analysis, because it calls shutdown unconditionally. No early consumer was established in its default configuration. |
| LSAPI | Modelled on FPM. Application-specific exploits considered likely, not validated. |
There is a detail inside the mod_php row that decides whether a workaround even reaches the vulnerable code. For CVE-2026-109619, .htaccess and the Apache php_value, php_flag and php_admin_* directives apply before request startup, and php.ini does not. Under FPM the early consumer is only exposed if the frontend passes PHP_VALUE or PHP_ADMIN_VALUE parameters, and pool settings and .user.ini do not provide the same early consumer. A setting applied in the wrong place looks like a mitigation and is not one.
How to patch PHP2Shell
- Inventory every PHP runtime you operate, not every site. One host often runs several interpreters: the distribution package, a vendor build under a control panel such as cPanel or Plesk, a container image, and a copy inside an appliance. The advisory applies to each of them separately.
- Update to the fixed release for your branch, meaning 8.5.12, 8.4.27, 8.3.36 or 8.2.35. On 7.4, 8.0 or 8.1, ask your distribution or LTS vendor for the backport, since Linux distributions and LTS vendors received the patches in advance of the public release.
- Restart every worker. Both bugs live in state a worker holds across requests, so a running worker keeps the old behaviour until it is replaced. Restart Apache or PHP-FPM. Then confirm with
php -vagainst the binary the web server actually loads. That is not always the one on your shell path. - Check your package source before you trust it. Sansec reported on release day that the volunteer-maintained Remi repository did not yet carry the fix, and the php.net announcement said the same. A package manager reporting no update available is not the same as being patched.
- Then go back and look for compromise. Patching stops the next attempt. It tells you nothing about the previous one, and there is credible reporting of exploitation before the patch was public.
Mitigations if you cannot patch PHP today
Take the official framing seriously here. The php.net advisory states that only an update is a complete solution, and that the measures below reduce exposure to specific attack paths without making an installation safe. Each one closes a named path and leaves the underlying class open.
For CVE-2026-109618, set memory_limit = -1 in a way the application cannot override. That means php_admin_value in the FPM pool or in the Apache configuration. Keep requests small with post_max_size = 8M for applications with no file uploads, or 32M where uploads are needed, with upload_max_filesize kept below it. If you bound memory at the container level, use a cgroup memory.max limit, and do not substitute RLIMIT_AS or RLIMIT_DATA, because those trigger the vulnerable allocation failure instead of preventing it.
memory_limit = -1 is the one mitigation to think twice about. It removes the limit that the attack abuses, and it also lets a single request consume all the memory on the server. On a shared or memory-tight host you may be trading a hard-to-exploit memory bug for a trivially reachable denial of service.
For CVE-2026-109619, set session.upload_progress.enabled = 0 and restart every worker, and on PHP 8.5 also set fatal_error_backtraces = 0 and restart again. Apply both through php.ini, an FPM pool or the server configuration, and do not rely on ini_set() or an FPM .user.ini, because neither runs early enough. Three more things help. Remove invalid or deprecated per-request settings that emit diagnostics before initialisation. Fix the fatal errors and execution timeouts an attacker can reach. And if your performance budget can absorb it, set pm.max_requests = 1, which stops a worker carrying state from one request into the next at all.
Two things that are widely recommended and do not work. Hiding diagnostics with error_reporting, display_errors or the @ operator does not help, because the stale state is read whether or not anything is printed. And a CMS maintenance mode does not help either, because PHP parses the request before the application decides to show a maintenance page. If you need to take an application off the air, stop PHP-FPM and serve a static page, or reject everything except GET and HEAD at the web server.
One broad control does have real coverage: block multipart/form-data at the frontend. Every known trigger reported so far arrives as a multipart request, and the advisory suggests this for any application that does not use multipart. It also breaks every file upload and every multipart form, so test a real submission afterwards.
How to tell whether PHP2Shell was exploited
Neither advisory publishes file hashes, addresses or paths, so detection is behavioural. The signal is the crash pattern the attack produces while it is being tuned, because the technique needs repeated attempts and the failures are noisy:
- Unusual multipart requests followed by memory limit errors or repeated HTTP 500 responses.
- A spike in PHP fatal errors around
memory_limit, particularly from multipart requests. - Unexpectedly large or malformed
PHPSESSIDresponse cookies. That is the shape memory disclosure takes on the upload-progress path. - PHP worker restarts or churn with no deployment or traffic event to explain them.
The public record on exploitation is genuinely mixed. It is worth being precise about who claims what. Cloudflare reported attacks against PHP sites at 07:43 UTC on 21 October, ahead of the 14:00 UTC release, and deployed WAF rules. watchTowr said it confirmed remote code execution on 20 October and later reported observations from its honeypot network. Patchstack said it had confirmed active exploitation incidents. Against that, NCSC-NL stated it had no indication of active exploitation. Sansec expected mass exploitation once the writeups and tooling were public, which is a forecast and not an observation. Read the disagreement as a reason to go looking in your own logs, since none of these parties can see your traffic.
How far back to look is a judgement call. Aikido advised hunting for compromise going back at least to June 2026. The reports to PHP were made on 11 August and 24 August 2026, and the underlying code is years older, so there is no date before which the bug did not exist. There is only a date before which nobody is known to have been using it.
What PHP2Shell changes about your attack surface
For most teams, "our attack surface" means the application: the endpoints, the authentication, the business logic an attacker can reach from outside. That is where exploitable bugs usually live and where testing should be hardest. PHP2Shell is a reminder that the surface also includes the thing running the application. A clean application buys no immunity when the interpreter underneath it can be driven into a use-after-free by a request that never reaches your code.
The other direction is the part that gets lost in a week like this one. A patched interpreter does not make an application safe. The reason PHP2Shell is frightening is that it needs no application bug, and the reason most real compromises happen is that there was one. Both statements are true at the same time. A team that spends the next month on PHP versions and none of it on its own authorisation logic has moved its risk around instead of reducing it.
The discipline is the same in both places: a finding counts once you have watched the system behave, not when a version number or a feed says so. An interpreter is not vulnerable because a scanner matched a banner, and an endpoint is not safe because a scanner found nothing. That is what we do for the half of the surface an attacker can reach right now, the running application and its APIs: authenticated, context-aware tests that validate each finding with a repeatable exploit before it reaches your pipeline.
Related reading: arrayref: 86 Minutes of Compromise - Vulnerability Prioritization with CVSS and EPSS