PHP Docker Vulnerability: Rebuild Your Images Post-PHP2Shell

docker pull doesn't correct PHP2Shell. See how to rebuild php:8.x and FrankenPHP images and what to change in CI/CD for the patch to reach production.

by Cleverson Gouvêa

PHP Docker Vulnerability: Rebuild Your Images Post-PHP2Shell

A vulnerable PHP Docker image doesn't fix itself when PHP releases a patch. The container in production continues running the binary it was created with, even after Docker Hub has published the new version. In this guide, I show how to find every image affected by PHP2Shell, how to correctly rebuild php:8.x and FrankenPHP images, what to change in your CI/CD pipeline, and why the container's memory limit doesn't close the vulnerability.

TL;DR

  • PHP2Shell refers to two critical vulnerabilities (CVE-2026-109618 and CVE-2026-109619, CVSS 4.0 = 9.2) that allow remote code execution without authentication. The fix was released on 21/10/2026 in versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
  • Official php:8.x images with the patch have been on Docker Hub since 21/10. FrankenPHP patched it in v1.13.2, on the same day.
  • docker pull doesn't fix anything on its own. You need to rebuild with a new base, push, and recreate each container.
  • Old fixed tags, pinned digests, and build cache can keep the old version without warning.
  • Container memory limits (cgroup) protect availability; they don't close the vulnerability. The other CVE doesn't even depend on memory.

Why a vulnerable PHP Docker image remains vulnerable after the patch

On a traditional server, apt upgrade replaces the package, and restarting PHP-FPM resolves the issue. In a container, the logic is different: the image is immutable. The PHP running inside it was compiled and frozen at build time.

This creates three layers that need to be updated, in this order:

  1. The base image (php:8.4-fpm, php:8.3-apache, dunglas/frankenphp) needs to have the corrected version.
  2. Your application image, which inherits from the base via FROM, needs to be rebuilt and pushed to the registry.
  3. Running containers need to be recreated from the new image.

Skipping any layer leaves a vulnerable PHP Docker image in production, appearing updated. The most common scenario I see is a team running docker pull on the server, seeing "Downloaded newer image", and closing the ticket. The new base is on disk, but the old container remains active, and the application image hasn't even been rebuilt.

I've worked with PHP since 2008 and use containers daily, from development environments with DDEV to the services we deliver in bespoke system development. The difference between an applied patch and a "downloaded" patch is always this: the process that actually handles requests.

PHP2Shell in Two Minutes: What Changed Between 17 and 23 October

PHP2Shell refers to two vulnerabilities in the PHP core. The official php.net advisory states that "all PHP installations serving web requests" are at risk, citing PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, and LiteSpeed. The greatest risk, according to PHP itself, is mod_php: there, the attack is triggered before any application code.

  • CVE-2026-109618 (memory exhaustion corruption): PHP runs out of memory during request assembly, and cleanup treats half-processed data as valid.
  • CVE-2026-109619 (stale global state, a use-after-free): an internal state survives after memory has been freed, and the next request in the same worker uses this remnant.

The timeline explains the urgency. According to the Tenable FAQ, Amazon observed an attempted exploit on 17/10. Details leaked on 20/10, and the patch was released on 21/10. Palo Alto Networks identified over 22.6 million exposed instances, as reported by TechTarget. On 23/10, PHP published an additional alert (CVE-2026-115560) regarding an unlikely vector related to session upload progress, which will be addressed in the next regular release.

PHP Branch PHP Status Official php Image on Docker Hub
8.5 Fixed in 8.5.12 Updated on 21/10/2026
8.4 Fixed in 8.4.27 Updated on 21/10/2026
8.3 Fixed in 8.3.36 Updated on 21/10/2026
8.2 Fixed in 8.2.35 (end of life on 31/12/2026) Updated on 21/10/2026
7.4, 8.0 and 8.1 Only minimal source code backports No new image: the tag 8.1-fpm hasn't received a push since 31/12/2025
5.4 to 7.3 No fix Abandoned (the 7.4-fpm stopped on 15/11/2022)

I verified the dates in the last column directly with the Docker Hub API and the official image repository. The tag 8.4.27-fpm received its first push on 21/10, at 16:43 (UTC).

How to Discover Which Images Are Vulnerable

Before rebuilding, take an inventory of every vulnerable PHP Docker image you have. In companies with multiple projects, a forgotten container always appears: an old dashboard, a queue worker, or a staging environment that remained exposed.

Ask the Container, Not the Host

The host's PHP says nothing about the container's PHP, and a vulnerable PHP Docker image might be running on a server whose php -v shows 8.4.27. Also, don't rely solely on the CLI's php -v: Datadog warns that the command-line installation can differ from the runtime serving the web. Run the check inside each service:

# PHP version in each running container
for c in $(docker ps -q); do
  echo "$(docker inspect --format '{{.Name}}' $c): $(docker exec $c php -r 'echo PHP_VERSION;' 2>/dev/null || echo 'no php')"
done

# image used by the container vs. newest image on disk
docker inspect --format '{{.Image}}' my-container
docker image ls --digests php

Check the Registry, Not Just the Servers

Official images compile PHP from source code and record the version in the PHP_VERSION variable. You can audit without spinning anything up:

docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' registry.example.com/app:prod | grep PHP_VERSION

A detail that catches many people out: since the PHP in the official image doesn't come from a Debian or Alpine package, scanners that only read the system's package database might not detect it. Datadog itself recommends a separate inventory for compiled runtimes. If your vulnerable PHP Docker image doesn't appear in the scanner's report, that doesn't prove it's secure.

What to Include in the List

  • Web containers (FPM, Apache, FrankenPHP).
  • Queue workers, cron jobs, and consumers using the same image.
  • Staging and preview images generated by CI.
  • Third-party images (dashboards, internal tools) that embed PHP. This is often the vulnerable PHP Docker image no one remembers to rebuild.

Rebuilding php:8.x Images: The Step-by-Step Guide That Works

With the inventory complete, fixing a vulnerable PHP Docker image follows a short sequence. What changes from project to project is how the FROM instruction is written.

Floating Tag, Fixed Tag, or Digest

How FROM is written Example Receives patch automatically? What to do
Branch tag php:8.4-fpm Yes, on the next build with --pull Rebuild with --pull
Exact tag php:8.4.26-fpm No Edit to 8.4.27-fpm and rebuild
Digest php:8.4-fpm@sha256:... No Update the digest and rebuild
Unsupported branch php:8.1-fpm Never Migrate the application to 8.2+

Exact tags and digests are good practices for reproducibility. The cost is precisely this: in an urgent fix, someone needs to edit the file. If your project uses a digest, the vulnerable PHP Docker image remains locked until a human (or a dependency bot) changes the line.

The Commands

# 1. rebuild with a new base (and without cache, if the cache holds the old version)
docker build --pull --no-cache -t registry.example.com/app:2026-10-24 .

# 2. check the version BEFORE publishing
docker run --rm registry.example.com/app:2026-10-24 php -r 'echo PHP_VERSION, PHP_EOL;'

# 3. publish and recreate the containers
docker push registry.example.com/app:2026-10-24
docker compose pull && docker compose up -d --force-recreate

# 4. validate what's running
docker compose exec web php -r 'echo PHP_VERSION;'

The --no-cache option is important. Without it, a cached layer might reuse the old base, and the build finishes "green" with the same vulnerable PHP Docker image as before. Recreating the container restarts all PHP workers. Mounted volumes remain intact.

Variants: Update All

Each variant is a separate image: -fpm, -apache, -cli, -zts, Debian (bookworm, trixie), and -alpine. Start with the -apache variants, which use mod_php, the scenario PHP considers highest risk. And do not expose the FPM port (9000) outside the internal network: FastCGI was not designed to be exposed.

FrankenPHP: Docker Image and Static Binary

FrankenPHP deserves a separate chapter because it comes in two formats, and each updates differently.

Docker Image

The v1.13.2 release, on 21/10/2026, brought images with PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12, explicitly citing PHP2Shell. Two days later, v1.13.3 was released, which fixes crashes (SIGILL) of musl static binaries on older CPUs. A vulnerable PHP Docker image based on FrankenPHP is corrected in the same way as official images. If your Dockerfile uses dunglas/frankenphp:1-php8.4 or 1.13-php8.4, a rebuild with --pull resolves the issue. If you use a previous fixed tag, such as 1.13.1-php8.4, edit it to 1.13.3 or higher.

Static Binary

Here lies the trap. The FrankenPHP static binary embeds PHP within the executable. Updating the operating system changes nothing. Updating the distro's php package also doesn't. The only fix is to replace the binary with a version that includes PHP 8.5.12 (v1.13.2 or later) and restart the service.

Anyone packaging the entire application into a single executable (FrankenPHP's "embed" feature) also needs to redo that build. This is precisely the case Datadog describes as "applications that bring their own PHP interpreter".

Worker Mode

In worker mode, FrankenPHP keeps the application loaded in memory between requests. CVE-2026-109619 specifically exploits state that survives from one request to the next in the same worker. There's no shortcut: update and restart all workers.

CI/CD Pipelines: How to Avoid Being Caught Out Again

PHP2Shell demonstrated the size of the window between "patch published" and "patch running". In a poorly designed pipeline, a vulnerable PHP Docker image can remain in production for weeks. In a good pipeline, it's a matter of hours.

What to Configure

  1. Scheduled rebuild. A weekly job (and a manual trigger) that rebuilds the image with --pull, even without a new commit. An updated base doesn't generate a commit, so a pipeline that only runs on push will never pick up the patch.
  2. Dependency bot for FROM. Renovate and Dependabot can open PRs updating image tags and digests. This maintains reproducibility and gains speed.
  3. Minimum version gate. A step that fails the build if PHP is below the minimum patched version:
docker run --rm "$IMAGE" php -r 'exit(version_compare(PHP_VERSION, "8.4.27", ">=") ? 0 : 1);' \
  || { echo "PHP below patched version"; exit 1; }
  1. Tag by date or commit, never just latest. This way, rollback points to a known image, and the registry shows what's in production.
  2. Final image scanning, with the aforementioned caveat: check the PHP version via the PHP_VERSION variable, not just the package database.

What it Costs

Keeping every vulnerable PHP Docker image under control costs little. The direct cost is small: minutes of CI per week and some extra registry space. The real cost is engineering time, from a few hours to a few days to standardise Dockerfiles, create the job, and test the rollback. Compare this with the cost of an unauthenticated intrusion on an exposed server.

For those running Moodle in a container, the logic is the same, with an extra precaution: check the PHP version matrix for each Moodle release before switching branches. The server requirements for Moodle can help with this assessment.

Why Container Memory Limits (cgroup) Are Not a Complete Mitigation

This is the point I see most often confused. Since one of the vulnerabilities involves memory exhaustion, it seems logical that a --memory=512m on the container would resolve it. It doesn't. A vulnerable PHP Docker image with a memory limit remains vulnerable.

What PHP Actually Recommends

The official mitigation for CVE-2026-109618 is memory_limit=-1, applied in a way that the application cannot override it (php_admin_value in the FPM pool). This removes PHP's controllable ceiling. However, the advisory itself admits that "an underlying allocation failure can still reach the vulnerable path".

Since memory_limit=-1 allows a script to consume all RAM, PHP suggests using a container or cgroup limit "as an availability barrier", preferably with a policy that kills only the affected worker. The keyword is availability. It is not security.

Four Reasons Not to Trust cgroup

  1. CVE-2026-109619 has nothing to do with memory. It exploits stale state between requests. No RAM limit interferes with it.
  2. The limit protects the host, not PHP. When the container exceeds the cgroup, the kernel kills a process. This prevents the entire server from crashing, but it doesn't fix the corruption the attacker is seeking.
  3. RLIMIT_AS and RLIMIT_DATA make it worse. PHP explicitly asks not to use these process limits instead of cgroup, because they can specifically trigger the vulnerable allocation failure. Be careful with ulimit -v in entrypoint scripts: it applies exactly an RLIMIT_AS.
  4. Killing the wrong process brings down the service. If the OOM killer targets the FPM master process, the entire container restarts. Repeated in a loop, this becomes a denial-of-service attack provoked by the attacker themselves.

How to Build Layered Defence While Updating

Layer Configuration Role
PHP memory_limit=-1 via php_admin_value Removes the predictable trigger for 109618
PHP session.upload_progress.enabled=0 Closes confirmed paths for 109619
PHP 8.5 fatal_error_backtraces=0 Closes the fatal backtrace path
Container --memory / limits.memory in Kubernetes Availability barrier
Proxy/WAF size limit, fields and multipart parts Reduces known payloads

Datadog summarises it correctly: these changes "reduce exposure while you update, but none fix the vulnerabilities". If you want to reinforce the edge, the guide on how to protect web servers against attacks covers WAF, fail2ban, and rate limits.

When NOT to Do It: Costly Pitfalls

In emergency fixes, the most common mistake is rushing in the wrong areas. Avoid these:

  • Do not use release candidate tags. Tags like 8.5.12RC1 are not the final version. Use only stable tags.
  • Do not run apt upgrade inside a running container. Besides not replacing the compiled PHP in the official image, the change disappears on the next restart.
  • Do not rely on ini_set() or .user.ini for mitigation. PHP warns that they are applied too late, after the request has been read.
  • Do not rebuild an 8.1 image expecting a fix. It won't come. For 7.4, 8.0, and 8.1, PHP only published minimal source code backports; the realistic solution is to migrate to 8.2+, preferably 8.3 or 8.4, as 8.2 goes out of support on 31/12/2026.
  • Do not do everything directly in production. Swapping a vulnerable PHP Docker image for one from another branch without testing simply exchanges one problem for another. A branch change (e.g., 8.1 to 8.4) can break extensions and legacy code. Patching within the same branch (8.4.26 to 8.4.27) is low risk. A branch change requires staging/UAT.
  • Do not forget host hardening. The container runs on a Linux host that also needs to be up-to-date; the Linux server hardening checklist covers this layer.

And after updating, look for signs that an attack occurred before the patch. Datadog lists indicators such as out-of-memory errors with "Unknown on line 0" in the log, segfaults (signal 11), and, most directly, a web worker opening a shell. If any of these appear, treat it as an incident, not maintenance.

How Agathas Web Can Help

If you have one or more PHP applications in containers and aren't sure that no vulnerable PHP Docker images remain in production, this is precisely the kind of work we deliver through our system development and support service.

What's Included

  • Complete inventory: running containers, registry, workers, cron jobs, and staging environments, with the actual PHP version of each.
  • Rebuild and publication: FROM update, cache-free build, push, container recreation, and validation of the running version.
  • FrankenPHP and static binaries: binary or image replacement, including in applications packaged with embedded PHP.
  • Well-applied temporary mitigations while branch migration is ongoing, at the correct level (FPM pool, not .user.ini).
  • Future-proof pipeline: scheduled rebuild, dependency bot, minimum version gate, and tested rollback.
  • Legacy PHP migration (7.4, 8.0, 8.1) to a supported branch, with staging/UAT before production.

How it Works in Practice

We start with a brief diagnostic: you provide us with read access to your registry and servers, and we return a list of what's vulnerable and the recommended order of correction. We then execute the rebuild within an agreed window, with rollback readiness. The same team handles Moodle, Linux servers, managed hosting, and integrations with the official WhatsApp API, so your entire application benefits from a unified perspective, not just the container.

How to Engage Our Services

Contact us via the Agathas Web system development page, describe your stack (how many projects, which images, where they run), and we'll respond with a scope and timeline. For urgent PHP2Shell-related matters, please state this in your initial message.

Conclusion: A Patch Only Counts When It Reaches the Process

The lesson from PHP2Shell for Docker users is simple. The patch was released on 21/10, and official images and FrankenPHP were updated on the same day, but none of this protects a container that hasn't been rebuilt and recreated. Inventory, rebuild with --pull --no-cache, push, recreation, and version validation: that's the sequence. Container memory limits help keep the service running, but they don't close the vulnerability.

If you want to end this week with the certainty that no vulnerable PHP Docker images remain, and with a pipeline that catches the next vulnerability within hours, contact Agathas Web.