PHP2Shell.fyi Check a site

How PHP2Shell works

How each bug works, which server setups were tested, what the fix changes and how this checker looks for exposure. The page explains mechanisms only, with no exploit code or attack steps.

Last reviewed . Rules of .

Overview

Both bugs come from state that PHP leaves behind when it stops work early. On a fatal or out-of-memory (OOM) error, PHP takes an emergency exit called a bailout. In some places that exit leaves internal data in a state that later code still trusts.

The PHP ChangeLog lists a single fix for both advisories, "Avoid state corruption via bailout", in 8.2.35, 8.3.36, 8.4.27 and 8.5.12. The PHP project rates both CVEs 9.2 (Critical) on CVSS v4.0.

CVE-2026-109618: out-of-memory corruption

Before the fix, PHP left the current context with longjmp() when memory ran out. During some steps of a request, request memory is reachable before it is fully set up. If any allocation fails at that moment, PHP does not account for the early exit, even when the failed allocation has nothing to do with the half-built data. Request shutdown can then read or branch on the invalid memory. The advisory says several ways of running out of memory reach this state reliably and lead to code execution.

Ways a request can run out of memory

The advisory names three gaps in multipart request parsing. Each lets a request run out of memory while it still appears to be within PHP's limits:

Application code can also run out of memory in its own ways, for example when attacker input causes one large allocation or repeated memory use.

Why the server setup matters

CVE-2026-109619: stale state across requests

A PHP worker keeps some global state between requests, such as error, session, object and extension state. Some of it points into memory that belongs to a single request, so PHP must reset it before it frees that memory. Some input, configuration, session and error steps run before the next request is fully set up, and they can read state that the previous request did not reset. That is a use-after-free. With a carefully shaped heap it can lead to full code execution.

The producer and consumer model

An attack needs two parts. The producer is a PHP operation that stores request-owned state where a later request can reach it. The consumer is a later operation that reads, frees or acts on that stale state. The advisory lists five conditions:

  1. A reachable operation stores request-owned state in a global place (the producer).
  2. The state is not reset, or another operation fills it again after its normal reset.
  3. PHP frees the request memory while the worker still holds the global reference.
  4. The same worker handles another request.
  5. An operation uses the stale state before normal request setup resets it (the consumer).

In the demonstrated cases, the producer and the consumer run in different HTTP requests on the same worker. More workers and worker recycling lower the success rate per request, but the condition stays.

Producers

State stays behind when a fatal error stops an operation before its matching reset. Fatal interruptions include:

A non-fatal warning from extension or server cleanup can also fill the error state again after shutdown already cleared it.

Confirmed consumers

The advisory confirms two consumers that run before normal request setup:

Early INI consumer examples

The advisory gives these examples of settings that raise a diagnostic early. On an unpatched PHP, such a setting applied before request setup can act as a consumer:

Whether this matters depends on where the setting comes from:

Hiding errors does not help. PHP touches the global state before it decides whether to report, show or log a warning. Changing error_reporting, using @, or turning off display_errors or log_errors gives no defence against this CVE.

Confirmed cases

Validation status per server setup

What the two advisories say about each way to run PHP. "Validated" means the researchers showed a complete attack. Everything else rests on code analysis or is unknown.

Sources: GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp and the php.net advisory of 2026-10-21. IIS and Caddy rows are our inference.
SetupCVE-2026-109618CVE-2026-109619
Apache mod_phpValidated over HTTP: code execution with default input and memory limits and a normal multi-worker prefork setupValidated: code execution through the PHP 8.5 fatal backtrace path and the session upload progress path
PHP-FPMAt risk by code analysis; no complete attack shownValidated: code execution through session upload progress, with a suitable application operation. The INI path applies only when the frontend sends PHP_VALUE or PHP_ADMIN_VALUE
FrankenPHPLikely open to attacks that need no application, by code analysis; no attack shownNo early INI consumer found in the default configuration; not immune
LSAPI (LiteSpeed)Attacks through a specific application likely; not validatedConditional candidate for the early INI path
php-cgiNamed at risk by php.net. The advisories give no separate detail.
IISNot named. Our inference: the PHP manual runs PHP on IIS through php-cgi, so the php-cgi row applies.
CaddyNot named. Our inference: php_fastcgi passes requests to a FastCGI server, usually PHP-FPM, so the PHP-FPM row applies. Caddy with FrankenPHP: see the FrankenPHP row.

What the fix changes

Soft and hard out-of-memory limits

The patch stops using longjmp() for out-of-memory errors and sets two limits instead:

Request shutdown no longer sees invalid state, and PHP still limits memory use.

memory_hard_limit_percent

The fixed releases add this INI setting, which sets the hard limit as a percentage of memory_limit. We checked it in the source of 8.5.12 and 8.2.35.

Wider state reset

For CVE-2026-109619, the patch resets the wider error, exception, object and extension state, so it cannot survive into steps that run before normal request setup.

After you update

Regression note: GH-24488

php-src issue GH-24488 reports zend_mm_heap corrupted crashes in 8.4.27 and 8.5.12. The crash happens when SimpleXML element properties are materialised inside a garbage collection cycle. The report ties it to the fix for issue GH-24050. It is a crash, not a reported vulnerability. On 2026-10-22 the issue was closed with a fix in the php-src source (commit f963484725). No PHP release contains it yet.

Do not downgrade: that brings back both critical bugs. Watch php.net for a follow-up release.

How this checker detects exposure

The normal check is passive. An optional active scan, which the user starts separately, sends test requests instead. The normal check sends at most three ordinary GET requests: the submitted URL, plus redirects to the same host or its www twin. It reads the same kind of response that a normal visitor gets. The operators page lists the exact requests and limits.

What it reads

Limits

Why the normal check sends no exploit-shaped requests

A checker could try to confirm these bugs by sending requests shaped like the attack. The normal check does not.

So the normal check sends no request bodies, no multipart data, no uploads, no payloads and no requests to other paths.

The optional active scan is the exception, for owners who want a stronger signal. It sends a few multipart test requests from our scanner server, so the risks above apply to it. That is why it is a separate action: it needs its own confirmation that the user owns the site or is authorized to test it, a new human check, and it runs at most once per site every 10 minutes. Its result is still an indicator, not a confirmation: a crash signal or an unpatched version banner needs checking against the installed PHP package, and no signal does not show that the site is not affected.

Sources

Check your site

See which PHP signals your site sends to the public. The check is passive. Check my site.