Patching php2shell in My Alpine Lighttpd PHP Docker Image
If you use my alpine-lighttpd-php Docker image, please pull :latest again and recreate your container. The image now runs PHP 8.4.27, which fixes the php2shell vulnerabilities.
docker pull alastairhm/alpine-lighttpd-php:latest
# or
docker pull ghcr.io/alastairhm/alpine-lighttpd-php:latestWhat is php2shell?
php2shell is the name for two PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619. Both are critical, and both allow remote code execution with no authentication. PHP published a security advisory on 21 October, alongside fixed releases 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
- CVE-2026-109618, RCE via OOM corruption. An out-of-memory error can interrupt PHP while internal request data is only partly set up. Request cleanup can then treat that invalid data as valid.
- CVE-2026-109619, RCE via stale global state. Some request-owned internal state can survive after its memory has been freed. A later request handled by the same worker can use that stale state before normal setup resets it.
The advisory says every PHP installation that serves web requests is at risk, including PHP-FPM, Apache mod_php, FrankenPHP, LiteSpeed and php-cgi, which is what this image uses. PHP 8.1 and older don’t get fixes from the PHP project.
The advisory lists some ways to reduce exposure if you can’t update straight away. They include setting session.upload_progress.enabled=0 for CVE-2026-109619, and setting memory_limit and post_max_size carefully for CVE-2026-109618. These settings only block specific attack paths. As the PHP team puts it, “there are no complete mitigations, updating is the only safe choice.”
How I found out
A user of the image, @gmanic, opened issue #5 asking whether I’d rebuild the image. I’m glad they did. The :latest image was shipping PHP 8.4.16, so it was vulnerable.
Why a rebuild wasn’t enough
Normally this image just installs PHP from Alpine’s package repositories, so a security fix means a rebuild once Alpine publishes new packages. This time the Alpine 3.23 mirrors were still serving php84 8.4.21, and I didn’t want to leave a publicly downloadable image exposed to an unauthenticated RCE until they caught up.
The fix: build PHP from Alpine’s own recipe
Rather than compile PHP from scratch, I added a temporary build stage to the Dockerfile. It builds php84 with Alpine’s own 3.23 APKBUILD, with only the version number changed. That way the packages match what Alpine will eventually ship, with the same configure flags, file layout and package split.
The php-build stage:
- Starts from
alpine:3.23withalpine-sdk, and creates abuilderuser and a signing key forabuild. - Does a shallow, sparse clone of aports on the
3.23-stablebranch, fetching onlycommunity/php84. - Changes
pkgverin the APKBUILD to 8.4.27 and resetspkgrel. - Downloads the source tarball and checks it against the SHA-256 published on php.net before building. If the checksum doesn’t match, the build fails.
- Runs
abuild -rto produce the.apkpackages.
RUN sed -i \
-e "s/^pkgver=.*/pkgver=$PHP_VERSION/" \
-e "s/^pkgrel=.*/pkgrel=0\noptions=\"!check\"/" \
APKBUILD && \
abuild checksum && \
echo "$PHP_SHA256 /var/cache/distfiles/php-$PHP_VERSION.tar.xz" | sha256sum -c - && \
abuild -rThe final stage, still on top of my alpine-lighttpd base image, copies those packages and the signing key in and installs from a local repository. The build then checks that it got what it expected:
php-cgi84 -v | grep -q "^PHP $PHP_VERSION " && \
! apk info -v | grep '^php84-' | grep -v -- "-$PHP_VERSION-" && \If php-cgi84 isn’t 8.4.27, or any installed php84-* package came from somewhere else, the image doesn’t build. I’d rather have a failed build than one that quietly falls back to the vulnerable version.
The downside is build time. Alpine’s APKBUILD compiles PHP twice, once for the Apache module and once for the other SAPIs, so a full build now takes about 20 minutes instead of seconds. That’s fine for a stopgap. Once Alpine 3.23 ships php84 8.4.27 or later, I’ll remove the stage and go back to a plain apk add.
Things I found along the way
Looking closely at the package list turned up a couple of other problems:
php-imapnever worked. The unversionedphp-imappackage installed the PHP 8.3 extension, which the PHP 8.4 runtime never loaded. Alpine has nophp84-imap, so I removed it.php-jsonwas redundant. JSON is built into PHP 8.4.- A suggested quick fix wouldn’t have worked here. Another commenter on the issue posted a one-line
docker execthat finds a PHPconf.ddirectory and writes the mitigation setting into it. On this image it could pick/etc/php83/conf.d/, which PHP 8.4 doesn’t read, so it would look like it worked without protecting anything. If you apply the mitigation by hand, use/etc/php84/conf.d/.
This release also includes some earlier fixes from PR #4. lighttpd and the php-cgi workers now run as the base image’s lighttpd user. Dotfiles like .git and .env are blocked again. The examples now mount a dedicated html/ directory instead of the current directory.
A CI slip
The GitHub Actions workflow ran with push: true on pull requests as well as on master. That meant the unmerged PR build for the fix was pushed straight to ghcr.io/alastairhm/alpine-lighttpd-php:latest before I’d merged it. This time it was the fixed image, so no harm done, but it shouldn’t happen. PR #7 changed it so pull requests only build, tagged pr-<number>, without logging in or pushing. Only pushes to master publish :latest.
Testing
Before merging I built the image locally and checked that:
php-cgi84 -vreports 8.4.27, and everyphp84-*package is8.4.27-r0- all the extensions the image provided before still load (iconv, gd, curl, xml, SimpleXML, pgsql, pdo_pgsql, soap, ldap, dom and so on)
- the container stays up,
index.phprendersphpinfo(), and raw.phpsource is never served - static files return 200 and
/.git/configreturns 403
The patched image went out on Docker Hub first, then ghcr.io, and it’s tagged as release 3.23.5. gmanic confirmed their pod updated without problems.
Thanks again to gmanic for raising it. If you run anything PHP-based in containers, it’s worth checking which PHP version your images ship, because a rebuild only helps once the distro has the fix.