A magnifier inspecting a stack of host records beside three PHP worker processes, one highlighted
TABLE Of CONTENTS

PHP2Shell: Your Version Number Is the Wrong First Question

Omair
2026-11-09
22
min read

Your team patched PHP in the last week of October. The ticket is closed. Then someone asks whether you were exposed, and for how long, and you discover the version number in the ticket cannot answer either question.

‍

That is the uncomfortable thing about PHP2Shell. The fix is easy to describe and easy to apply. Establishing whether you were reachable, and whether anything reached you, depends on three facts most asset inventories do not carry: which PHP SAPI your application runs under, how many worker processes it runs, and whether session upload progress was enabled.

‍

What is PHP2Shell?

PHP2Shell is the informal name for two distinct critical vulnerabilities in PHP core, CVE-2026-109618 and CVE-2026-109619, disclosed and patched on 21 October 2026. Both are rated CVSS v4.0 9.2 by the PHP project. Both can lead to remote code execution without authentication. They are not two views of one bug: they have different mechanisms, different preconditions, different affected SAPIs, and different mitigations.

‍

Which PHP versions are affected, and what fixes them?

Both advisories list the same four affected ranges: earlier than 8.2.35, earlier than 8.3.36, earlier than 8.4.27, and earlier than 8.5.12. The fixes are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. PHP 8.6.0 release candidates before RC4 also carried the bugs.

‍

PHP 8.1 and older no longer receive updates from the project, but they are not stranded. Given the severity, the security team published minimal source-backports for PHP 7.4, 8.0 and 8.1 on php-src. If you are on one of those branches your options are those backports, your distribution's packages, or an upgrade — not "wait for a patch that is not coming." Note also that the 8.2.35 announcement states PHP 8.2 reaches end of life on 31 December 2026, so 8.2.35 buys weeks, not years.

‍

One date to be careful with: the PHP release archive dates the 8.5.12 release 21 October 2026, while the ChangeLog heading for 8.5.12 reads 22 October 2026. The project's two pages disagree, and we have not resolved which is authoritative. Treat 21 October as the disclosure date and do not build an exposure-window calculation on single-day precision the sources do not support.

‍

There is also a third issue in flight, and it is worth describing precisely rather than ominously. The project's 23 October archive entry says it "will publish a full advisory for CVE-2026-115560 with the next release," and calls it a vector reachable only where an application sets session.serialize_handler to php_binary or php_serialize in configuration and then switches it to php at runtime with ini_set() before calling session_start(), with session upload progress still enabled. The project describes that combination as unlikely in practice and is handling it in the regular release cycle. If that is not your application, it is not your problem.

‍

Why isn't the version number enough?

Because the exploitability of CVE-2026-109618 depends on your SAPI, and the PHP project says so explicitly.

‍

The CVE-2026-109618 advisory states that "complete remote code execution was validated over HTTP against mod_php with default PHP input and memory limits and normal prefork configurations with multiple workers." For other SAPIs that advisory is deliberately less certain: FPM is susceptible "though no complete FPM exploit has been demonstrated for this SAPI," FrankenPHP is "likely susceptible to application-independent attacks," and for LSAPI it records "application-specific exploits being likely but unvalidated."

‍

Here is the part the coverage tends to drop, and it reverses the comfortable conclusion. That FPM caveat belongs to CVE-2026-109618 only. The CVE-2026-109619 advisory states that "session lifecycle RCE was validated against mod_php and FPM." So an FPM pool is not the safer half of the estate — it is less demonstrated for one CVE and validated for the other.

‍

Which is the actual point. "Are we on a patched PHP?" has a cheap yes/no answer. "Were we exploitable, and how reliably?" has four answers, one per CVE per SAPI, and an inventory that records PHP 8.3.31 and nothing else collapses all four into a single green tick. The project's own at-risk list is wider than the two SAPIs that get discussed: it names "all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed."

‍

A SAPI (Server API) is the interface through which a web server hands a request to PHP. mod_php, PHP-FPM, php-cgi, FrankenPHP and LiteSpeed are different SAPIs, and for PHP2Shell they carry materially different demonstrated risk.

‍

Side-by-side comparison of CVE-2026-109618 and CVE-2026-109619 showing mechanism and preconditions for each

‍

What are the two bugs, actually?

CVE-2026-109618 is a state corruption problem in out-of-memory handling. PHP uses longjmp() to abandon a request on OOM, and the advisory notes that "in some contexts, request memory is temporarily reachable before it is fully initialized." An allocation failure at that moment leaves request memory in an invalid state that the shutdown procedure then reads or branches on. The named attack paths are multipart requests without a usable declared content length, unlimited headers per multipart part, and empty file controls with nested field names — plus the caveat that "OOM can also be reached in application-specific ways." Apache mod_php is the worst case because it "unconditionally invokes the request shutdown procedure, even when request startup has failed."

‍

CVE-2026-109619 is a use-after-free arising from stale global state — "error, session, object and extension state that is retained by a worker but points to memory owned by one request," accessed by a later request before initialization replaces it. Critically, it requires two operations: "successful exploitation of this vulnerability requires both a producer and a consumer," and "the demonstrated producing and consuming operations occur in different HTTP requests handled by the same worker."

‍

That last clause is the one that matters operationally. Producer and consumer must land on the same worker process. The advisory is clear that worker count changes the odds but not the exposure: "multiple workers and recycling reduce success per request but do not remove the condition."

‍

Is session upload progress the mechanism for both?

No, and this is a widely repeated error. The session upload progress path belongs to CVE-2026-109619, not to CVE-2026-109618 — so a write-up that pins that field, or the advice to disable it, on CVE-2026-109618 has the mapping wrong.

‍

It is also worth being precise about what role it plays, because getting that wrong is the same class of mistake. The advisory notes that "session upload progress is enabled by default, and no application call to session_start() is required," which is what makes it reliably reachable, and says it "can supply a producer, consumer or both." The advisory's named producer in its strongest demonstrated case is a different setting: with fatal_error_backtraces enabled, fatal error reporting stores backtrace state owned by the request.

‍

A related confusion is worth naming separately. "Session file poisoning" — writing PHP into a session file through upload progress, then including it from a later request — is an older technique, and it is not the mechanism in either October advisory. If a write-up describes PHP2Shell as session file poisoning, it is describing an older bug class or a tool's probe shape, not what the advisories document.

‍

Is 70% of the internet affected?

No. That figure is PHP's market share, not an exposure measurement.

‍

The number derives from surveys of the share of websites whose server-side language is known and is reported as PHP. It counts deployments of PHP. It does not count deployments of an affected version, under a susceptible SAPI, reachable by an unauthenticated request. Those are three successive filters, and none of the coverage we reviewed applied any of them. Used as an exposure number it tells a buyer nothing actionable and makes the genuinely useful questions look like detail. If someone hands you a percentage, ask what it is a percentage of.

‍

Matrix showing how demonstrated risk differs by CVE and by PHP SAPI across mod_php, PHP-FPM, php-cgi, FrankenPHP and LiteSpeed

‍

How do you determine whether you were exposed?

Work through the deployment, not the version string. Each of these is a check with an answer you can evidence.

‍

Enumerate PHP by SAPI, not by package. php -v on a shell tells you the CLI binary's version, which may not be the version your web server loads. Query the running web path instead, and record the SAPI name alongside the version for every host.

‍

Record worker configuration. For CVE-2026-109619, producer and consumer must share a worker. Prefork worker counts, FPM pm.max_children, and recycling settings change the probability per attempt. Record them as context for likelihood, never as mitigation — the advisory is explicit that they do not remove the condition.

‍

Check the effective value of session.upload_progress.enabled, at the right layer — and restart every worker. It is on by default. The project's 21 October release advisory says to "Use php.ini, an FPM pool or server configuration that applies before request parsing" and warns: "Do not rely on ini_set() or FPM .user.ini." It also states the measure "blocks the confirmed session paths but not other variants." A codebase grep for ini_set is not an answer to this question, and a config change without a worker restart is not a change.

‍

Check fatal_error_backtraces on PHP 8.5. The CVE-2026-109619 advisory calls the 8.5 fatal error backtrace path "the strongest demonstrated case and is the basis of the confirmed mod_php exploitation," and recommends setting fatal_error_backtraces=0 administratively — again, with every worker restarted.

‍

For CVE-2026-109618, know which measure is the primary one. The advisory states that "setting and enforcing memory_limit=-1 administratively is the primary configuration mitigation." A container or cgroup memory limit is a different control — the advisory offers it as an availability boundary and says it "is not guaranteed to prevent allocation failure and is not a complete mitigation." The project adds: "Do not substitute RLIMIT_AS or RLIMIT_DATA, as these can cause the vulnerable allocation failure." Worker recycling via pm.max_requests=1 is named as a further measure where performance allows.

‍

Decide what your logs can and cannot tell you. The CVE-2026-109619 advisory states that there is "no complete generic configuration mitigation as an application, extension or server path may publish stale state" — and notes such a path may also consume that state without producing a predictable log entry. An absence of suspicious log lines is not evidence that nothing happened. Say so in the incident note rather than letting silence read as a clean result.

‍

Do not treat the partial mitigations as a patch. The project's position, from its 21 October release announcements: "There are no complete mitigations, updating is the only safe choice." The configuration measures are worth applying where a patch is genuinely delayed, but the project describes them as partial.

‍

Four-step sequence for determining PHP2Shell exposure: enumerate by SAPI, record workers, check effective ini values, scope what logs can prove

‍

What are the honest limits of testing for this?

Testing can establish reachability and configuration with high confidence. It cannot give your estate a clean bill of health on exploitation.

‍

Reachability is testable: whether an unauthenticated request reaches PHP before application code runs, which SAPI serves it, what the effective ini values are at the layer that matters, and whether the multipart shapes the advisory names are accepted.

‍

Reliable code execution is a different matter, and the CVSS vector says why. Both CVEs score 9.2 with AC:H and AT:P — high attack complexity, attack requirements present. The CVE-2026-109618 advisory adds that "exploit portability and reliability depend on the PHP build, extensions, frontend behavior and allocator layout." Published demonstrations of working execution have carried their own caveats about running against deliberately favourable single-worker mod_php profiles, which their authors correctly described as not a measurement of reliability against other worker counts, builds or applications.

‍

The consequence is specific. A test that fails to achieve execution against your build tells you something about that attempt, not about your safety. That is the difference between a control that has not been broken yet and a control proven to hold, and it is why we would rather report "unauthenticated requests reach PHP under mod_php with upload progress enabled, patch not yet applied" than "we tried and could not get a shell." The first is evidence; the second is an anecdote about an afternoon.

‍

That gap is also the honest answer to what a scanner gives you here — the session file poisoning misdescription plausibly originates in exactly this confusion, a probe shape read back as the vulnerability's mechanism. Scanners enumerate version and configuration faster than any human, but their output is not an exposure finding until someone has mapped it to the advisory. We have written before about how a fluent, confident, wrong technical answer is the hardest kind to catch and why a faster scanner is not a penetration test.

‍

Was PHP2Shell exploited before it was disclosed?

On the available evidence, no — and the timeline saying otherwise does not hold up.

‍

This is worth getting right because it is the claim most likely to end up in a board paper, so note the provenance of each step. The PHP project pre-announced the release on 20 October and reserved "the right to release earlier should details of either vulnerability become public, or should evidence of active exploitation appear in the wild" — the conditional phrasing of a project that had not seen exploitation. That sentence is from a primary source. Everything after it is not.

‍

Vendor sensor networks reported a working proof of concept circulating on 20 October, the first PHP2Shell-shaped requests on 21 October, the disclosure day, and broader scanning following the release. One operator reported confirmed code execution against a honeypot on 24 October, with the favourable-profile caveat noted above. We have not independently verified any of those four observations, and none of them appears in a PHP advisory.

‍

What the sequence supports, on that evidence, is that the earliest observed activity lands on the disclosure day itself. We found no source with its own telemetry claiming exploitation days before disclosure. Neither PHP's advisories nor its archive entries claim in-the-wild exploitation at all. If you need this for a board paper, cite the vendor and call it vendor telemetry.

‍

One loose end we could not close: the two CVEs' status in CISA's Known Exploited Vulnerabilities catalog. Reporting from late October disagrees on whether CISA added CVE-2026-109618 alone or both CVEs on 26 October, and CISA's own pages refused our automated requests while we were checking. If your remediation deadlines are driven by the KEV catalog, confirm the entry yourself rather than taking a secondary source's word for the scope.

‍

Three corrections to widely repeated PHP2Shell claims: the 70% figure, the session mechanism, and the pre-disclosure timeline

‍

What does a mid-market estate actually look like?

The following is a synthetic composite, assembled to illustrate the pattern. It is not a client.

‍

A 300-person company runs a customer portal on PHP 8.3 behind nginx with FPM, a legacy reporting application on an older Apache host with mod_php, and a marketing site on a managed platform. The patch ticket covered the portal, because the portal is what the asset register treats as production. The mod_php host did not appear in the register as a PHP asset at all — it appeared as a reporting server. The managed platform's PHP version is the vendor's to control, and nobody has asked them in writing.

‍

Three exposure positions, three remediation owners, one version number in the ticket. The post-incident question is answerable for exactly one of the three. That is an inventory problem wearing a patching problem's clothes: if your application security programme tracks frameworks and dependencies but not SAPIs and effective ini values, a PHP2Shell-shaped event will find the same gap.

‍

What good looks like after a breaking CVE

The pattern that works is not faster patching. It is having the facts a patch decision needs already on file before the CVE lands: an inventory keyed on SAPI and effective configuration rather than package version, a documented answer to "which hosts accept unauthenticated requests that reach a runtime before application code," and a standing relationship with whoever controls the runtime on managed platforms. All of that is the kind of thing a penetration test with properly scoped rules of engagement produces as a by-product and a DevSecOps pipeline can keep current between tests.

‍

It also means being honest in the artefact. A finding should distinguish "reachable and unpatched" from "reachable, patched, exploitation not demonstrated against this build" from "not reachable." Collapsing those into one red row is how a remediation programme patches the wrong host first — and the same discipline applies whether the runtime sits in a cloud environment, on internal network infrastructure, or in the software supply chain you inherited with a dependency.

‍

Prove, don't assert. For PHP2Shell that is not a slogan; it is the difference between an exposure answer and a version number.

‍

Frequently asked questions

Is PHP2Shell one vulnerability or two?

Two. CVE-2026-109618 is a state corruption bug in out-of-memory handling. CVE-2026-109619 is a use-after-free arising from stale global state that requires a producer request and a consumer request handled by the same worker. They share a disclosure date and a CVSS score of 9.2, not a mechanism.

‍

Which PHP versions fix PHP2Shell?

PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12, released 21 October 2026. PHP 8.1 and earlier no longer receive regular updates, but the security team published minimal source-backports for 7.4, 8.0 and 8.1 on php-src given the severity. Note that PHP 8.2 itself reaches end of life on 31 December 2026.

‍

Does disabling session upload progress fix it?

No. Setting session.upload_progress.enabled=0 in php.ini, an FPM pool or server configuration — and restarting every worker — closes one confirmed CVE-2026-109619 path, and the project warns not to rely on ini_set() or FPM .user.ini to do it. The project says the measure "blocks the confirmed session paths but not other variants," that "there are no complete mitigations, updating is the only safe choice," and this measure does nothing at all for CVE-2026-109618.

‍

Is PHP-FPM safe?

No. The "no complete FPM exploit has been demonstrated" line belongs to the CVE-2026-109618 advisory only, and it is widely quoted as though it covered both. The CVE-2026-109619 advisory states that "session lifecycle RCE was validated against mod_php and FPM." Apache mod_php carries the highest demonstrated risk for CVE-2026-109618; FPM has a validated path for CVE-2026-109619. Patch regardless of SAPI.

‍

Was PHP2Shell exploited before disclosure?

No source with its own telemetry supports that. Vendor sensor networks report a proof of concept circulating on 20 October 2026, the first exploitation-shaped requests on 21 October, and confirmed code execution against a honeypot on 24 October against a deliberately favourable configuration. That is vendor telemetry, not advisory text — PHP's own advisories do not claim in-the-wild exploitation at all.

‍

ioSENTRIX Can Help

ioSENTRIX is a CREST-accredited penetration testing firm, ISO/IEC 27001 certified and SOC 2 Type 2 attested. When a runtime vulnerability like PHP2Shell lands, the work that reduces risk is rarely the patch itself — it is establishing, with evidence, which systems were reachable, under which configuration, and what your logs can and cannot prove about the window before you patched. That is an adversarial exercise, and independent adversarial testing is what produces an answer you can put in front of a board or an auditor rather than an assurance that a ticket was closed.

‍

If you want a second pair of eyes on your exposure position — or a penetration test scoped to answer the exposure question rather than restate the CVE — we are happy to have that conversation.

‍

Keep reading

No items found.
Contact us

Similar Blogs

View All