PHP2Shell (CVE-2026-109618, CVE-2026-109619): patch the PHP inside a container image before upstream rebuilds it

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (October 21, 2026) fix PHP2Shell, two unauthenticated remote code execution bugs in PHP itself (CVSS 9.2, attacks seen the same day). An image that bundles PHP, such as phpMyAdmin, Roundcube or shinsenter/php, gets the fix only when its maintainers rebuild it. Hours after PHP's release, phpmyadmin:latest (8.3.35), roundcube/roundcubemail:latest (8.4.26), shinsenter/php:8.3-fpm (8.3.35) and shinsenter/php:8.2-fpm (8.2.34) still shipped vulnerable PHP.

These two files recompile the PHP inside an image you already run, from the php.net source, without waiting:

  • Dockerfile builds on your image, reads the image's own configure flags (php-config --configure-options), checks the source against php.net's SHA-256 and the PHP release managers' GPG keys, and installs the new PHP over the old one. A patch release keeps the extension API, so the image's extensions and its config stay as they are.
  • php-rebuild.sh IMAGE TAG finds the newest php.net release of the image's PHP branch, skips an image that already has it, builds it and prints the new version.

It works on Debian-based images built on the official php image (php:*-apache, php:*-fpm, phpmyadmin, roundcube/roundcubemail, shinsenter/php), not on Alpine variants.

Use

Download both files (the Raw buttons above) into an empty folder, then:

CONTAINER_ENGINE=docker bash php-rebuild.sh roundcube/roundcubemail:latest local/roundcube:php-fixed   # podman is the default
docker run --rm --entrypoint php local/roundcube:php-fixed -v

Run your container on the new tag, or tag it with the image name your compose file uses and recreate the container. A build takes 2 to 15 minutes depending on the CPU count.

What to watch when you deploy it

  • Roundcube keeps its SQLite database inside the container unless you mounted a volume on /var/roundcube/db. Copy sqlite.db out before you recreate the container. The new container's first start copies Roundcube into place and only then creates its database, so wait for resuming normal operations in the logs before copying the old one back.
  • If you tag the rebuild with the upstream name, your next pull brings back upstream's image. Check that the new upstream image is fixed before pulling: docker run --rm --entrypoint php -r 'echo PHP_VERSION;'.
  • podman with a second image store (--root ) needs its own --runroot too. With the default one, a pull there unmounts your running containers' layers, and their next restart fails.

Tested

On 2026-10-21, in production on an OpenPanel server: phpMyAdmin 8.3.35 → 8.3.36, Roundcube 8.4.26 → 8.4.27, shinsenter/php 8.5.11 → 8.5.12, 8.3.35 → 8.3.36 and 8.2.34 → 8.2.35, with ionCube still loading. With Docker 29: roundcube/roundcubemail:latest 8.4.26 → 8.4.27, the same 45 modules as before, no startup warnings.

On OpenPanel, the full guide with the service-by-service steps (phpMyAdmin, Roundcube, users' PHP through the shared image store) is stefanpejcic/OpenPanel#1204.

# Rebuilds an image's PHP from a newer upstream patch release, in place, for a PHP security release
# that lands before the image's maintainers rebuild (PHP2Shell, CVE-2026-109618 and CVE-2026-109619).
# Works on any docker-library based image (php, phpmyadmin, roundcube/roundcubemail, shinsenter/php):
# the image's own configure flags (`php-config --configure-options`), the source checked against
# php.net's SHA-256 and the release managers' GPG keys ($GPG_KEYS, set by the base image). A patch
# release keeps the extension API, so the image's own shared extensions and config load unchanged.
#
# podman build --build-arg BASE= --build-arg PHP_NEW= \
# --build-arg PHP_NEW_SHA256=.tar.xz> -t .
#
# php-rebuild.sh fills in the version and checksum. Guide:
# https://openpanel.com/docs/articles/security/php2shell-patch-php-before-images-are-rebuilt
ARG BASE
FROM ${BASE}
ARG PHP_NEW
ARG PHP_NEW_SHA256
RUN set -eux; \
apt-mark showmanual > /usr/src/apt-manual.saved; \
apt-get update; \
apt-get install -y --no-install-recommends $PHPIZE_DEPS ca-certificates curl gnupg xz-utils \
libargon2-dev libcurl4-openssl-dev libonig-dev libreadline-dev libsodium-dev libsqlite3-dev \
libssl-dev libxml2-dev zlib1g-dev \
$(case "$(php-config --configure-options)" in *--with-apxs2*) echo apache2-dev;; esac); \
cd /usr/src; \
curl -fsSL -o php.tar.xz "https://www.php.net/distributions/php-$PHP_NEW.tar.xz"; \
curl -fsSL -o php.tar.xz.asc "https://www.php.net/distributions/php-$PHP_NEW.tar.xz.asc"; \
echo "$PHP_NEW_SHA256 *php.tar.xz" | sha256sum -c -; \
export GNUPGHOME="$(mktemp -d)"; \
for key in $GPG_KEYS; do gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"; done; \
gpg --batch --verify php.tar.xz.asc php.tar.xz; \
gpgconf --kill all; rm -rf "$GNUPGHOME"; \
mkdir php-new; tar -Jxf php.tar.xz -C php-new --strip-components=1; cd php-new; \
# --with-pear only fetches PEAR from the network at install; the image's PEAR stays as it is.
opts="$(php-config --configure-options | sed 's/ build_alias=.*//; s/ --with-pear//')"; \
CFLAGS="$PHP_CFLAGS" CPPFLAGS="$PHP_CPPFLAGS" LDFLAGS="$PHP_LDFLAGS" \
PHP_BUILD_PROVIDER='https://github.com/docker-library/php' PHP_UNAME='Linux - Docker' \
./configure $opts; \
make -j"$(nproc)"; \
find -type f -name '*.a' -delete; \
make install; \
cd /; rm -rf /usr/src/php-new
# docker-library's own cleanup, in its own layer so a failure here does not cost the compile: keep
# every library something in /usr/local links against (diversion lines are skipped, as upstream does).
RUN set -eux; \
apt-mark auto '.*' > /dev/null; \
[ ! -s /usr/src/apt-manual.saved ] || apt-mark manual $(cat /usr/src/apt-manual.saved); \
rm /usr/src/apt-manual.saved; \
find /usr/local -type f -executable -exec ldd '{}' ';' \
| awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); printf "*%s\n", so }' \
| sort -u | xargs -r dpkg-query --search | awk 'sub(":$", "", $1) { print $1 }' | sort -u | xargs -r apt-mark manual; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*; \
php -r 'exit(PHP_VERSION === getenv("PHP_NEW") ? 0 : 1);'; \
# An extension that no longer loads is a startup warning, not an exit code.
if php -m 2>&1 | grep -iE 'warning|unable to'; then exit 1; fi
LABEL php-rebuilt="${PHP_NEW}"
#!/bin/bash
# php-rebuild.sh IMAGE TAG
#
# Rebuilds the PHP inside IMAGE from the newest php.net release of its own branch and tags the result
# TAG, for a PHP security release that lands before the image's maintainers rebuild it (PHP2Shell,
# CVE-2026-109618 and CVE-2026-109619). IMAGE is any docker-library based image: php, phpmyadmin,
# roundcube/roundcubemail, shinsenter/php. Only PHP changes: the image's own configure flags, its
# extensions and its config are kept. Needs podman (or CONTAINER_ENGINE=docker), curl, jq and the
# Dockerfile next to this script. Debian-based images only (the Dockerfile uses apt-get).
#
# Guide: https://openpanel.com/docs/articles/security/php2shell-patch-php-before-images-are-rebuilt
set -euo pipefail
image=${1:?usage: php-rebuild.sh IMAGE TAG}
tag=${2:?usage: php-rebuild.sh IMAGE TAG}
dir=$(cd "$(dirname "$0")" && pwd)
engine=${CONTAINER_ENGINE:-podman}
cur=$("$engine" run --rm --entrypoint php "$image" -r 'echo PHP_VERSION;')
branch=${cur%.*}
release=$(curl -fsS "https://www.php.net/releases/index.php?json&version=$branch")
new=$(jq -r '.version' <<<"$release")
sha=$(jq -r '[.source[] | select(.filename | endswith(".tar.xz")) | .sha256][0]' <<<"$release")
[[ $new =~ ^[0-9]+\.[0-9]+\.[0-9]+$ && $sha =~ ^[0-9a-f]{64}$ ]] || { echo "php.net has no release for PHP $branch"; exit 1; }
if [[ $(printf '%s\n' "$cur" "$new" | sort -V | tail -n 1) == "$cur" ]]; then
echo "$image already has PHP $cur (newest $branch release: $new)"
exit 0
fi
echo "$image: PHP $cur -> $new"
"$engine" build --build-arg BASE="$image" --build-arg PHP_NEW="$new" --build-arg PHP_NEW_SHA256="$sha" \
-t "$tag" -f "$dir/Dockerfile" "$dir"
echo "$tag: PHP $("$engine" run --rm --entrypoint php "$tag" -r 'echo PHP_VERSION;')"
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论