Blog / English

PHP2Shell: why your composer.lock won't save you from CVE-2026-109618

October 22, 2026 — Equipe ArchGenerator — php · security · vulnerability-management · patching · sdd

The ticket arrives at 09:40 with a link and three words: upgrade PHP. An engineer opens composer.json, sees "php": "^8.2", runs composer update, gets no changes, and closes the ticket as resolved. Production is still running the same base image it was built from fourteen months ago, and nothing about that image changed.

That is the shape of the PHP2Shell response in a lot of organizations right now. The vulnerability is real, the patch exists, and the work is being routed to the wrong place.

What was actually disclosed

On October 21, 2026, the PHP project published security releases for two critical vulnerabilities, both scored 9.2 under CVSS v4.0. According to Rapid7's writeup, CVE-2026-109618 is a memory corruption issue triggered during out-of-memory handling, while CVE-2026-109619 is a use-after-free involving state retained between requests. Both allow unauthenticated remote code execution with the privileges of the web worker (Rapid7).

The pre-release picture was messier. watchTowr's FAQ, written before the official advisory, described an unverified report claiming impact on every PHP version from PHP 5 onward, with no confirmed technical cause or fixed version at that moment (watchTowr). Huntress noted at the time that it had not built a proof of concept and was not aware of public exploit code, while also describing a real incident pattern worth watching: a PHP FastCGI process repeatedly writing a one-line PHP file into the first writable temp directory it could find, executing it, then deleting it (Huntress).

That pattern matters more than the CVE text. It describes what post-exploitation looks like on a PHP host — not a dramatic payload, but a small file in /tmp that lives for two seconds.

The patch is in a layer your dependency manager cannot see

Composer resolves PHP libraries. It does not resolve the PHP interpreter. Those are different supply chains with different owners, different release cadences and different inventories — and in most organizations only one of them is tracked.

The interpreter shows up in places nobody put on the architecture diagram:

Where PHP livesHow it usually gets there
Container base imagesFROM php:8.3-fpm, or a distro image with php-fpm installed
Distro packagesapt install php8.2, patched by the distribution, not by php.net
CI runnersA build image pinned two years ago
Developer machinesHomebrew, asdf, phpenv, Docker Compose
Serverless layersA Lambda layer or a buildpack-produced artifact
Vendor appliancesA third-party product that embeds PHP and never mentions it
Shared hostingSomeone else's upgrade schedule

A composer.lock diff proves nothing about any of these. Neither does a green pipeline: if the pipeline builds on a cached image, it will happily rebuild the same vulnerable runtime and report success.

This is the part that gets lost in the rush to close tickets. PHP2Shell is not a flaw in a library you can bump. It is a flaw in the process that executes your code, and the only fix is replacing that process.

The disclosure fight, and why it changes your timeline

The release itself became a dispute. The PHP Foundation acknowledged that signals of one of the flaws had existed since 2024 and criticized Chainguard for publishing corrected images ahead of the coordinated time; Chainguard justified the early release by pointing to reports of exploitation already underway (PHP Foundation). Fixed versions were published as 8.2.35, 8.3.36, 8.4.27 and 8.5.12 (php.net archive).

You do not have to settle that argument to draw the operational conclusion from it: your patch window may be shorter than the embargo assumed. If images existed before the announced hour, so did the ability to diff them and reverse-engineer the fix. Treat the disclosure date as the start of your exposure, not the start of your planning.

That has a concrete consequence for incident process. When a critical runtime flaw lands with contested timing, the question "are we affected?" has to be answerable in minutes from inventory, not days from interviews. If answering it requires asking four people which base image production actually runs, the answer will arrive after the decision it was supposed to inform.

A remediation path that survives an audit

1. Inventory the runtime, not the manifest

Start from artifacts, not repositories. For every deployed workload, you want the interpreter version and the digest of the image or package that provides it. A crude but effective first pass:

# per running container
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}' \
  | while read -r ns name image; do echo "$ns $name $image"; done

# per host
php -v

Then reconcile against the fixed versions. Anything below 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 on a supported branch is in scope; anything on an unsupported branch is a different conversation about whether it can be patched at all.

2. Patch the artifact and pin it

Rebuild the base image on the fixed tag, or upgrade the distro package if the distribution has shipped its own backport. Then pin by digest, not by tag — php:8.3-fpm is a moving reference and will silently drift again. Update the CI image in the same change, or the next build reintroduces the problem.

3. Verify against the running process

A merged pull request is not evidence. The evidence is the version string reported by the process serving traffic, plus the image digest that produced it. Capture both, per environment, and keep them with the incident record. If a workload cannot be rebuilt — a vendor appliance, a legacy host — say so explicitly and record the compensating control instead of pretending the box is patched.

4. Treat WAF rules as time, not as a fix

Edge rules can filter known exploitation patterns and are worth deploying while images rebuild (Cloudflare). They do not remove memory corruption from the interpreter. A WAF rule that blocks one request shape leaves the flaw in place for the next shape.

5. Watch the right signals afterwards

Given the post-exploitation pattern Huntress described, the useful detections are boring: PHP processes writing executable files into temp directories, unexpected child processes under the web worker, new files in webroots with recent mtimes. None of these are PHP2Shell-specific, which is exactly why they are worth having.

Where ArchGenerator fits

The two CVEs are catalogued and considered in the security assessments, and findings from an assessment can feed remediation work through SDD kits. The relevant discipline here is the one this incident demands anyway: the assessment states what it observed and where the coverage stops, so an interpreter that was never in scope is declared as a gap rather than scored as healthy.

Remediation kits are generated from the Remediation Center, after you select the problems for a repository — the assessment reader routes you there with findings pre-selected. And the honest framing matters: a delivered kit, or a generated wave, is not proof that the runtime was upgraded or that the fix reached production. Updating PHP and validating the environment is execution work that belongs to your team, with its own evidence. The kit organizes the work; it does not perform it.


The uncomfortable lesson of PHP2Shell is not that PHP had a memory-safety bug. It is that most organizations cannot answer, quickly and with evidence, which interpreter version is serving their traffic. Until that question has a fast answer, every critical runtime advisory will be handled the same way: a ticket, a composer update, and a closed status that means nothing.

← All articles