Skip to main content
← All articles
self-hosting

PHP shipped two CVSS 9.2 RCEs out-of-band — my floating Docker tag couldn't be trusted to pick them up

PHP shipped two CVSS 9.2 RCEs out-of-band — my floating Docker tag couldn't be trusted to pick them up

On 20 October, php.net posted something you don't see often: a pre-announcement with no CVE numbers, no affected-version list, no technical detail at all — just a date, a time, and a severity. "Upcoming Critical PHP Security Release," scheduled for 21 Oct 2026 14:00 UTC, two vulnerabilities, both CVSS v4.0 9.2. That's the embargo working as intended: enough warning to plan a maintenance window, not enough detail to hand attackers a head start. My blog runs on PHP, so I had about a day to think about what "plan a maintenance window" should actually mean when you don't yet know what you're patching.

What actually shipped

The release landed on schedule. Per PHP's own changelog, the fixed versions are 8.5.12, 8.4.27, 8.3.36 and 8.2.35, patching:

  • CVE-2026-109618 — interrupted out-of-memory handling can leave request data partially initialized, and cleanup can then treat that invalid state as valid.
  • CVE-2026-109619 — request-owned internal state can outlive the memory backing it, and a later request on the same worker can end up using it.

Both are remote code execution, both CVSS 9.2. I'm not going to speculate about exploitation paths beyond what the vendor published — there was chatter circulating before the official release with claims of in-the-wild exploitation and even a packaged "exploit toolkit" floating around on an unrelated domain. None of that was corroborated by PHP or by anyone I'd trust to verify it, so I treated it as noise and waited for the primary source. That turned out to be the right call: the actual advisory, when it landed, was far more mundane and far more useful than anything in the pre-release rumor mill.

The part that actually tripped me up

My blog's Dockerfile builds on top of the official drupal:11-apache image, which bundles PHP. I checked my running container and found PHP 8.5.11 — one patch version behind, exactly what you'd expect the morning before a scheduled release. My Dockerfile line was simply:

FROM drupal:11-apache

That's a floating tag. In theory, once the upstream image rebuilds with the patched PHP, my next deploy should just pick it up — Coolify rebuilds on every push to main, and the Dockerfile re-pulls the base image. In practice, "should" is doing a lot of work in that sentence. Docker build layers get cached aggressively, including the base image reference itself, and depending on how your build host is configured, a rebuild triggered by an unrelated commit can silently reuse the base image layer it already has cached rather than checking upstream for a new digest. I had no way to prove, from the outside, that my next deploy would actually fetch 8.5.12 and not just redeploy 8.5.11 with a new container ID and a false sense of security.

So I didn't trust the floating tag. I pulled the image fresh, confirmed it now built PHP 8.5.12, grabbed its digest, and pinned the Dockerfile to that exact digest instead of the tag:

FROM drupal:11-apache@sha256:73377aba568d1b9443780c5f95526d39f9bd66abaef2aeb301f86ff6921f4b70

That's a one-line change, but it converts "probably gets the patch on next deploy" into "deterministically builds this exact, verified image, every time, regardless of what's cached where." If I need to bump PHP again next month, I re-pull, re-verify, re-pin — a few seconds of ceremony in exchange for never having to wonder whether a security fix actually landed.

What I verified before touching production

Same discipline as any core update: build locally first, don't find out about a broken build on the production host.

  • docker build locally against the pinned digest — clean build, no surprises.
  • php -v inside the built image — confirmed 8.5.12.
  • php -m — confirmed every extension Drupal needs (gd, pdo_mysql, mbstring, curl, xml, zip, opcache) was still present. A base-image bump doesn't touch the extension-install layer in my Dockerfile, but I'd rather check than assume.

Only after that passed did I merge to main and let the deploy pipeline run. After the rebuild: php -v on the live container confirmed 8.5.12, drush status showed a successful bootstrap, drush updatedb:status reported no pending database updates (expected — this was a base image bump, not a Drupal core update), and the site answered with a clean 200.

The actual lesson

A pre-announcement with no details isn't a gap in the process, it's the process. The useful response to "two critical RCEs, details withheld" isn't to go hunting for leaked specifics — it's to make sure your patching path is actually trustworthy before you need it. "My Dockerfile says drupal:11-apache, so I'll get the fix automatically" was a comfortable assumption that I couldn't actually verify. "My Dockerfile is pinned to a digest I pulled and tested myself ten minutes ago" is one I can. If you're running anything on a floating base-image tag for exactly this reason — "it'll pick up security patches automatically" — it's worth spending the five minutes to prove that's actually true, instead of finding out the hard way during the next embargo countdown.

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.