Field Notes


4 min read

Which of My Containers Run PHP?

On 21 October 2026, PHP shipped fixes for two critical remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, called “PHP2Shell” (advisory). The fixed releases are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Anything older on those branches is affected, and 8.1 and below get no fix at all.

I don’t write PHP. That doesn’t mean I don’t run it. A lot of the software I host comes as a Docker image, and some of those images have PHP inside without saying so on the label. The first question wasn’t “how do I patch?” but “where is PHP at all?”.

Pebble, a small red rock, inspects things with a magnifying glass between stacks of shipping containers.

The script

It goes through every running container, once per image, and looks for a PHP binary: php, php-fpm, php8 and so on. If it finds one, it asks that binary for its version.

Two choices make it work on any image:

  • It finds the binary with docker export and tar -t, not by running which php inside the container. Images built on scratch or distroless have no shell, so anything that needs one fails there.
  • It reads the version by calling the binary directly with docker exec, which also needs no shell.

It runs on Linux and on the bash 3.2 that macOS still ships, which is why it keeps the list of seen images in a string instead of an associative array.

#!/usr/bin/env bash
# Check every running container's image for a PHP binary (php, php-fpm, php8, ...)
# and print the PHP version if one is found.
#
# Detection uses `docker export` (works even on images without a shell, e.g.
# scratch/distroless). The version is read by running the binary directly with
# `docker exec <container> <binary> -v`, which also needs no shell.

set -euo pipefail

COL=80
PATTERN='(s?bin)/php(-fpm)?[0-9.]*$'

# newline-separated list of images already checked (macOS ships bash 3.2,
# which has no associative arrays, so `declare -A` is not an option)
seen_list=""

printf "%-${COL}s %s\n" "IMAGE" "PHP?"
printf "%-${COL}s %s\n" "-----" "----"

containers=$(docker ps --format '{{.ID}} {{.Image}}')

while read -r id image; do
  [[ -z "$id" ]] && continue

  # one check per unique image
  if grep -qxF -- "$image" <<< "$seen_list"; then continue; fi
  seen_list="$seen_list"$'\n'"$image"

  matches=$(docker export "$id" 2>/dev/null | tar -t 2>/dev/null | grep -E "$PATTERN" || true)

  if [[ -z "$matches" ]]; then
    printf "%-${COL}s %s\n" "$image" "no"
    continue
  fi

  # prefer the CLI binary (php, php8, ...) over php-fpm for the version lookup
  bin=$(echo "$matches" | grep -vE 'php-fpm' | head -n1 || true)
  [[ -z "$bin" ]] && bin=$(echo "$matches" | head -n1)
  bin="${bin#./}"

  version=$(docker exec "$id" "/$bin" -v 2>/dev/null | grep -oE 'PHP [0-9]+\.[0-9]+\.[0-9]+[^ ]*' | head -n1 || true)
  [[ -z "$version" ]] && version="version unknown"

  printf "%-${COL}s %s\n" "$image" "YES  $version  (/$bin)"
done <<< "$containers"

Download it as check_php_containers.sh, or fetch it on the server:

curl -fsSL https://panzeri.rocks/files/check_php_containers.sh -o check_php_containers.sh
bash check_php_containers.sh

Read a script before you run it on a server. That goes for this one too.

Then patch

Once I knew which images had PHP and which version, the fix was boring. In each Compose project:

docker compose pull
docker compose up -d --force-recreate

Pull the new images, then recreate every container so none of them keeps running the old one. Three minutes, and the problem was gone.

Run the script again afterwards. Pulling only helps if the maintainer has already rebuilt the tag with a fixed PHP. If a version still shows up below the fixed release, that image needs a rebuild, either by its maintainer or by you.

What it doesn’t do

  • It only looks at running containers. A stopped container with an old image is still a risk the next time it starts.
  • docker export writes the whole filesystem of a container to the pipe. On large images that takes a few seconds each.
  • It finds PHP binaries in bin and sbin directories. PHP compiled into another program, with no separate php executable, won’t show up.

Why this matters more than the patch

The patch was quick because I knew exactly where to apply it. Most of the stress in a security alert comes from not knowing whether you’re affected. Containers make that worse: you choose an image for the software, and its runtime comes along unasked. A script like this turns “am I affected?” into a table. Keep one for every runtime that tends to make the news.

More in Field Notes

Share