Components

php2shell: the bug that runs before your code does

Your application can be flawless and still hand over the server. Two critical PHP flaws, and the layer underneath your code that nobody reviews.

On 21 October 2026, the PHP project shipped PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 under a notice headed "Critical remote code execution vulnerabilities, update now". The two flaws it fixed, CVE-2026-109618 and CVE-2026-109619, are now being called php2shell. Both are in the PHP runtime itself, not in any framework or application, and the project's own advisory describes a demonstrated exploit "where an unauthenticated request can trigger the attack before any application code runs".

That last clause is why this post exists. Every other post in this series is about a mistake in application logic, which is the kind of thing a code review, a secure framework or a careful developer can prevent. Nothing in your code could have prevented this. A PHP application with perfect access control, parameterised queries and no file handling at all was still exposed, because the bug sits in the request parsing that happens before your code is loaded. The vulnerability class is the oldest one on the OWASP list and the least glamorous: vulnerable and outdated components.

CVE
CVE-2026-109618 (GHSA-w8hw-w9fx-j79w, "RCE via OOM corruption") and CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp, "RCE via stale global state")
Product
PHP before 8.2.35, 8.3.36, 8.4.27 and 8.5.12. PHP 8.1 and older get no upstream releases; the project has published minimal source backports for 7.4, 8.0 and 8.1.
Class
Vulnerable and outdated components (OWASP A06:2021). The underlying flaws are memory-safety bugs, including CWE-416 use after free.
Severity
CVSS v4.0 9.2 (Critical) for each
Disclosed
21 October 2026 by the PHP project, with alerts from the NCSC and CISA the same day. At the time of writing, CISA reports no observed exploitation.
Impact
Unauthenticated remote code execution with the web worker's privileges. The PHP project rates the risk highest for Apache with mod_php, and lists PHP-FPM, php-cgi, FrankenPHP and LiteSpeed as at risk too. CVE-2026-109619 also lists memory disclosure among its confirmed impacts.

The root cause, in one sentence

Code you did not write runs on every request, and it is as current as the last time someone updated it and restarted it.

The technical root causes are specific. In one, an out-of-memory condition interrupts PHP while it is still setting up a request, and cleanup then trusts the half-built data. In the other, state from one request outlives the memory behind it, and a later request on the same worker picks it up. The class, though, is not "memory corruption in PHP". It is the gap between the version of a component you think you are running and the one actually serving traffic. Exactly the same thing applies to your framework, your libraries, your web server, your base image and every product you bought that runs PHP inside it. The patch for the class is less about code than about inventory and process.

No payloads appear here. As in the other posts in this series, the mechanism is described to the depth a defender needs and no further. This one is a day old, so that matters more than usual. If you run PHP, work from the PHP project's advisory, not this page. It lists interim settings for teams that cannot update today, and it is blunt that "there are no complete mitigations, updating is the only safe choice".

The five places outdated components hide

When a client tells us they have patched, it is usually true of the server they were thinking of. These are the places where it is not.

1. The patched package and the unpatched process

Updating a package replaces the files on disk. It does nothing to a PHP-FPM master, an Apache parent or a long-running worker that loaded the old code into memory last month. Until that process restarts, the server reports the new version and runs the old one. The PHP advisory's interim settings have the same catch: each one says to "restart every worker". This is the most common way a "patched" system is still vulnerable a week later, and nothing in a package audit will show it.

2. End-of-life branches

PHP 8.1 and everything before it gets no upstream release for this. The project has published source backports for 7.4, 8.0 and 8.1, but applying them means rebuilding PHP yourself or relying on a distribution or LTS vendor who has. A team on an unsupported branch is not "behind on patching". It is on a version that has no normal way to receive a patch at all. PHP 8.2 joins that list on 31 December 2026.

3. Container images and pinned base layers

An image built in March contains the PHP that was current in March, forever. Updating the host does nothing to it, and a FROM line pinned to a minor version tag only moves if someone rebuilds and redeploys. We regularly find a carefully maintained host running containers that are months behind it, and the team was looking at the host.

4. PHP you did not know you had

The marketing site on a subdomain, the support desk, the analytics dashboard, the admin panel of a network appliance, the vendor product with a bundled web interface. Plenty of software runs PHP without saying so, and none of it appears in your composer.json. The NCSC's first recommendation is to find every PHP installation, "not just outdated versions", and NCSC-NL is maintaining a public list of affected products because that inventory is the hard part.

5. The layer above the runtime

Frameworks, libraries and plugins have exactly the same problem one level up. A Composer lock file pinned two years ago, a CMS plugin nobody owns any more, a JavaScript library vendored into a public/ directory. The runtime gets the headlines this week. The dependency tree is where we find this class most of the time, because it changes more often, more people touch it and it is spread more widely.

The reason we argue about the rating. Outdated components are often reported as "informational: version disclosed" and left at the bottom of the report. Two of PHP's interim mitigations for these CVEs involve fixing things testers report as low all the time: fatal errors an attacker can trigger, execution timeouts an attacker can reach, and diagnostics emitted on a request. A vulnerability that needs a precondition is only as hard to exploit as that precondition is to find. So we rate component findings in context, by what else in your application they combine with, not by the banner on their own.

How we test for it

  1. Fingerprint the stack from the outside, and record what we cannot see. Response headers, error pages, default files, cookie names and framework behaviour usually identify the runtime and often its version. Where a CDN or a hardened config hides it, we say so in the report. A missing banner is not evidence of a patched server, and automated scanning does not check banners before it tries.
  2. Ask for the running version, not the installed one. In a grey-box test we ask for the version reported by the process actually serving requests (PHP-FPM or the Apache module, not the command-line binary, which can differ) and when that process last started. A start time before the patch date is a finding, however up to date the package list looks.
  3. Map everything in scope that embeds a component. Every host on the scope list gets the same treatment, including the ones nobody thinks of as "the application": the CMS, the help desk, the admin interface of a product you bought. Where we find something that looks related but is out of scope, we flag it and do not test it.
  4. Check the dependency layer. With source or a lock file, we compare every dependency against published advisories and check whether the vulnerable code path is actually reachable. Black-box, we look for exposed manifests, version strings in static assets, and behaviour that pins a library to a version range.
  5. Report the preconditions as well as the version. Fatal errors an attacker can trigger, requests that hit execution timeouts, and verbose diagnostics are findings in their own right. Where a published advisory names them as conditions an exploit depends on, the report says so and rates them accordingly.
  6. Do not fire the exploit at production. We do not trigger memory corruption on a live system to prove a version is vulnerable. The likely result is crashed workers on your service, and the version and configuration evidence already answers the question. If a client wants a demonstration, that is a separate exercise against a staging copy, agreed in the rules of engagement before anything is sent.

Each finding is written up with the evidence, the affected hosts and the specific change that fixes it, rather than a generic "keep software up to date". That is the standard described in what's in a penetration test report.

Six things to check in your own estate this week

  • Update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's patched package, and then restart PHP-FPM, Apache and every long-running worker. Check that each process started after the update was installed.
  • Confirm the version from the process serving web traffic, not from php -v in a shell. The command-line binary and the web runtime are often separate packages.
  • Rebuild every container image that includes PHP from a patched base, redeploy it, and check that the running containers really are the new build.
  • List every system on PHP 8.1 or older and decide today whether it is upgraded, rebuilt with the backport, or taken offline. CISA and the NCSC both say offline is the right answer if neither of the others is possible soon.
  • Find the PHP you did not install yourself. Go through vendor products, appliances and hosted tools against NCSC-NL's list and each vendor's own advisories.
  • If an exposed system ran an affected version and you cannot rule out that it was reached, follow the NCSC's advice and check it for signs of compromise before you treat patching as the end of it. A patch closes the hole. It does not remove anything that already came through it.

Patching the runtime is an operations job, and this week it should come before any test. What a penetration test adds is the part a patch cycle does not cover. We check what is actually running against what you think is running, and we look for the low-rated findings that turn a published CVE into a working attack. We also cover the access control and business logic flaws that no update will ever fix. Testing is fixed price, agreed before we start, with the retest included.

Build your quote