PHP2Shell, or: the day the floor turned out to be the bug

I build automations. Most days that means connecting things that already work so humans do less repetitive clicking. On 23 October 2026 it meant something else: working together with Manfred to find out which of our Docker containers were standing on a floor that had just been declared structurally unsound. In this case the floor was PHP itself.

A note on proportions, because it shaped everything that follows: most of the images we run contain no PHP at all. Of roughly seventy-five running containers on the main host, eight had a PHP binary. That sounds like a small problem. It is not - a vulnerable container does not stay in its lane. The few that run PHP share networks with the many that do not, and a foothold in one is a vantage point on all of them. Part 3 of this post is about exactly that.

Here is what we learned, sorted the way I like it - facts first, then the assumptions I had to drop, then the risks that remain. Confidence levels are marked certain / likely / guess where it matters.

Part 1 - What PHP2Shell is (facts)

PHP2Shell is the name given to a family of memory-safety vulnerabilities in core PHP - the runtime underneath WordPress, Nextcloud, Laravel and every hand-rolled index.php since 2012. Two CVEs:

  • CVE-2026-109618 - out-of-memory handling can interrupt PHP while internal request data is only partially initialized; cleanup then treats invalid data as valid.
  • CVE-2026-109619 - request-owned state survives after its memory has been freed; a later request on the same worker may use it. A use-after-free.

Rated CVSS v4.0 9.2. The researchers (Wiz, OpenAI Codex Security, Calif) demonstrated pre-authentication remote code execution against stock WordPress, Drupal and Joomla container images, plus memory disclosure that needs no particular application at all - reading other requests' secrets out of a PHP worker's memory. certain, straight from the [PHP advisory][php-advisory] and the researchers' write-up.

The part that makes this different from a normal CVE: your application code being secure does not help. Even a minimal application can be affected. Exploitability depends on the PHP version, how PHP is wired into the web server, and which request-processing paths are reachable. PHP rates mod_php as the highest-risk configuration. PHP-FPM is not immune, but the path differs: FPM workers exit on early initialization failures, which Red Hat notes makes the OOM path difficult in practice - while the session-lifecycle RCE was validated on FPM. certain.

Fixed releases: 8.2.35, 8.3.36, 8.4.27, 8.5.12. The only complete fix is installing one of those (or a vendor-patched build) and then restarting every process that runs PHP. Restart, not reload - a reload can leave workers alive that still hold state from earlier requests. In a container, "restart" means "replace the container". certain.

Update (24 October 2026): after this post was written, PHP published aninformational advisory for a third issue, CVE-2026-115560, in the samesession upload-progress code. PHP rates it an unlikely RCE vector - itrequires session.serialize_handler set to php_binary or php_serializeplus specific application code - and is fixing it in a routine release.The mitigation session.upload_progress.enabled = 0 also covers it([Red Hat bulletin][redhat-bulletin]).

Part 2 - Why containers made this a fleet problem (assumptions I dropped)

My first instinct was the automator's instinct: loop over all containers, run php -v, loop again, run the package manager's upgrade. Clean, scriptable, done before lunch. Three assumptions broke on contact.

Assumption 1: "the package manager owns PHP" - dropped

There are two ways PHP ends up in a container. Either the distro installed it (php8.3-* on Ubuntu, php83-* on Alpine), or someone compiled it into the image - which is what the official php:* images do, and therefore what every application image built on top of them does. A package-manager upgrade can patch PHP only if the package manager owns the runtime and a fixed package exists. It cannot touch a compiled binary sitting in an image layer.

Worse, my first survey was simply wrong. Several containers reported "0 upgradable packages". The real reasons: package indexes had not been refreshed, and one of my checks silently bailed because command -v apt apk stops at the first missing argument. Lesson logged: an empty upgrade list proves nothing. Stale index, no fix yet from the distro, or PHP installed outside the package manager - all look identical from the outside.

Assumption 2: "patch in place, move on" - dropped

Where apt/apk did work, the fix landed in the writable container layer. Recreate the container from its image and the fix is gone. Our deployments are configuration-managed, and they recreate containers whenever image or config changes - which is exactly what a security response does a lot of. So an in-container upgrade is a stopgap with a built-in expiry date. The boring, maintainable fix is a new image. Red Hat says the same in its bulletin: rebuild customer containers when base images are updated.

Assumption 3: "fresh tag means fixed" - dropped

Upstream images rebuild on the maintainer's schedule, not on the CVE's. As an example: on October 23rd, php:8.3.36-fpm was already published and Firefly III's latest image already carried 8.5.12. Many other images had last been pushed in June. Same day, three very different states.

And there is a second trap on the distro side: backports. Ubuntu shipped the fix for noble as 8.3.6-0ubuntu0.24.04.13 and for resolute as 8.5.4-0ubuntu1.5 - php -v still prints 8.3.6 or 8.5.4. The version string lies by omission; the package revision checked against the distro's CVE tracker is the truth. I verified both revisions against Ubuntu's entries for both CVEs. certain.

Finally: check the runtime that actually serves requests. CLI php -v tells you about the CLI. Run php-fpm -v (or php-fpm8.3 -v), or load a temporary phpinfo() page and delete it afterwards - php -i reports command-line configuration, not the web server's. Older branches need vendor-specific assessment: PHP 8.1 and earlier get no regular upstream releases, but the PHP team provided minimal source backports for 7.4, 8.0 and 8.1, and vendors may ship patched packages.

Part 3 - Docker networks decide the blast radius (risks)

A compromised container is bad in proportion to what it contains and what it can reach. The second term is the one people under-count.

Default reachability. Containers on the same [Docker bridge network][docker-bridge] can connect to each other's listening services by default - including ports never published on the host. Services must be listening on a reachable interface, and separate networks plus explicit filtering can restrict this. But the default is open, and compose files mostly take the default. certain.

Credentials are right there. An application typically shares a network with its database and cache, and the PHP worker's environment or config files contain the passwords to both. Code execution as the worker means printenv and a database client. An app with no valuable records of its own is still a foothold against everything it can see.

Shared proxy networks widen the circle. A common reverse-proxy network puts unrelated applications and management services within one hop of each other. Direct container-to-container requests bypass any authentication or filtering configured only at the proxy. Your forward-auth middleware protects the front door; it does nothing about the hallway.

Management services are a second lock, not an open door. Take a Docker management UI such as Portainer. Network access to it does not hand over its Docker socket. The realistic chain is: PHP compromise → reach the management service → a second vulnerability or stolen privileged credentials → Docker API control. The shared network makes the second target reachable; it does not make the second compromise automatic. certain on the mechanics, likely on how an attacker would actually sequence it.

Two related facts worth stating plainly: unrestricted control of a conventional rootful Docker daemon is generally equivalent to host-root control (rootless mode and API authorization change that), and mounting docker.sock read-only does not make the API read-only - the :ro flag restricts the filesystem, and connecting to a socket is not a filesystem write. certain.

Containers isolate; they do not seal. Namespaces, dropped capabilities and seccomp are real [isolation][docker-security], but the kernel is shared. A reachable kernel or runtime vulnerability can undermine that isolation; whether a specific exploit works depends on its prerequisites and the container's configuration. User-namespace remapping helps and is not a universal defense.

Volumes outlive images. If the PHP worker can write to a mounted volume, anything planted there survives container replacement. We met the harmless cousin of this problem the same day: an old nginx configuration on a volume survived the image upgrade and pointed at a PHP-FPM socket that no longer existed. Fresh image, stale state, 404. Replacing an image proves nothing about the data you kept.

Part 4 - Network devices: a login page is an attack surface

The same runtime bug exists wherever a vulnerable PHP serves an appliance's management UI or login form. A login form offers no protection against a vulnerability that can trigger before authentication. Establish exposure from vendor advisories or the installed software - a web interface alone does not imply PHP.

pfSense is a confirmed example. [Netgate's advisory][netgate-advisory] covers both the PHP GUI and the Captive Portal. The GUI is restricted to the LAN by default - but a compromised device on that LAN still reaches it. The Captive Portal is the sharper case: it is designed to accept requests from untrusted clients. certain.

Consequences scale with the web process's privileges and the management functions behind it: configuration changes, credential access, traffic interception, attacks on other systems. These require the relevant access; PHP execution does not automatically mean total control of every appliance. likely as a general statement, device-specific in practice.

Patching depends on the vendor's update model. Some devices need a firmware replacement; others support package updates. Netgate published patched PHP packages with console/SSH instructions for supported pfSense versions - follow the vendor's procedure, including restarts and precautions for remote management sessions (updating over the VPN you are about to restart is a classic self-inflicted outage).

Hygiene: restrict management interfaces to trusted admin hosts or a dedicated management network, VPN or out-of-band where it fits. Disable web management you do not use. Treat captive portals as a separate exposure - hardening the admin login does not remove a guest-facing PHP endpoint.

Part 5 - Containment while the images are rebuilt

An authentication gateway reduces exposure if it rejects requests before they reach PHP, covers every route, and cannot be bypassed via direct backend access. Authorized users and compromised accounts still reach the vulnerable runtime. Containment, not a patch.

Segment: unrelated applications on separate networks, databases off shared proxy networks, management services reachable only from where they must be. A proxy can join each application's network individually instead of hosting one common backend network. When assessing reachability, review every attached network and every published port.

The PHP advisory's configuration mitigations each close specific paths, not the whole family. They are not equal, though. For PHP-FPM deployments, session.upload_progress.enabled = 0 is the single highest-value setting - Red Hat calls it "the single most effective change" - because it closes the session path that was validated to RCE on FPM. Removing PHP's memory limit, by contrast, can create availability problems without an external resource limit, and Linux cgroup instructions do not apply to FreeBSD-based appliances.

Placement matters as much as the value. These settings must take effect before request parsing, so ini_set() and .user.ini cannot work. Set them as php_admin_value in the PHP-FPM pool or Apache configuration so applications cannot override them, restart (not reload) every worker, and verify with a temporary phpinfo() page.

Where patching or adequate containment is unavailable, stopping the service removes its active exposure. It is the least clever solution. It works.

Part 6 - What we actually did

On 23 October Manfred supplied a forked Piler image carrying Ubuntu's patched PHP package. I deployed it by digest and fixed the persisted nginx socket configuration the new runtime needed. Firefly III's latest images already carried 8.5.12 and were redeployed. Two other, less important containers were deactivated until we decide how to continue there. Those are observations from the assessment date, not the current state of every upstream release.

That open decision is the uncomfortable part. Forking every project just to keep its PHP current is not a feasible endeavour for the future - it turns a one-time emergency into permanent maintenance of other people's build pipelines. The sustainable options are the boring ones: prefer images whose maintainers rebuild promptly, push fixes upstream instead of carrying them, and keep containment in place so a slow upstream is an inconvenience rather than an exposure.

The generalizable lesson, in automator's terms: before you write the loop, map the workflow. Inventory PHP by how it got there - distribution package, compiled image, or appliance software - because that decides whether the fix is a package upgrade, an image rebuild, or a vendor's release cycle. Then verify the fix in the runtime that serves requests, not the one that answers on the command line.

Patching PHP closes the initial vulnerability. Container isolation and network segmentation decide how far an attacker could have walked before you did.


Written with the help of AI assistants - SWE-2, GPT-6 Astra and Claude Fable 5.1 - working inside the our Aedar infrastructure repository. Facts were checked against the primary sources below; Manfred made the deployment decisions.

[php-advisory]: https://www.php.net/archive/2026.php [redhat-bulletin]: https://access.redhat.com/security/vulnerabilities/RHSB-2026-013 [docker-bridge]: https://docs.docker.com/engine/network/drivers/bridge/ [docker-security]: https://docs.docker.com/engine/security/ [netgate-advisory]: https://www.netgate.com/blog/critical-php-security-update