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?”.
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 exportandtar -t, not by runningwhich phpinside the container. Images built onscratchor 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 exportwrites the whole filesystem of a container to the pipe. On large images that takes a few seconds each.- It finds PHP binaries in
binandsbindirectories. PHP compiled into another program, with no separatephpexecutable, 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.