PHP2Shell and Docker: the self-hosted apps quietly running PHP

By Seba Kubisz · · 14 min read · Self-Hosted

On October 21, 2026, the PHP project shipped fixes for two critical bugs in PHP itself. Wiz Research, which found them with others, calls the pair PHP2Shell. Chained together, they let someone with no account run code on the server, and Wiz demonstrated it against the stock wordpress, drupal, and joomla container images. Cloudflare said it was already seeing attack traffic the same day.

If you self-host, the natural reaction is "I don't run WordPress." That's the trap. A good share of the containers in a typical self-hosted stack are PHP applications that never mention PHP: the speed-test dashboard, the budgeting app, the RSS reader, the webmail, the wiki. Each image carries its own copy of PHP, and each one has to be rebuilt by whoever publishes it before docker compose pull fixes anything.

This guide covers how to find the PHP in your stack, which popular self-hosted apps have it, why some images get patched within hours and others take days, and what to do until yours is fixed.

The short version: find out which of your containers run PHP and which version. Take anything internet-facing off the internet, or put real authentication in front of it, until a rebuilt image exists. Then pull, and check the version again, because a fresh image isn't always a patched one. A handful of popular images won't get the fix from a routine rebuild at all, and for those the answer is a different image or a nudge to the maintainer.

What PHP2Shell is

PHP2Shell is two vulnerabilities, CVE-2026-109618 and CVE-2026-109619, each rated 9.2 (critical) under CVSS 4.0. They are bugs in the PHP runtime, not in any application, so an app's own updates don't fix them and its login page doesn't stand in the way. Wiz's write-up says even a "Hello World" script on an affected version can be vulnerable.

Simplified: one bug lets a crafted upload run PHP out of memory while a request is only half set up, and the cleanup code then trusts memory that was never valid. The other lets internal state from one request outlive it, so a later request handled by the same PHP worker uses memory that has already been freed. Both happen while PHP is still processing the incoming request, before the application's code, and its login check, ever runs.

  • Fixed in PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35, all released October 21.
  • No official release for 8.1 and older. The PHP security team published minimal source patches for 7.4, 8.0, and 8.1 for people who build PHP themselves, and some Linux distributions backport fixes to their own packages. A Docker image built on the official php:8.1 image, or anything older, gets neither.
  • Server setups. The advisories confirm remote code execution on Apache with mod_php for both bugs, and on PHP-FPM through the session-handling path of CVE-2026-109619. They call FrankenPHP "likely susceptible" to CVE-2026-109618. Most self-hosted PHP images run one of the first two.
  • Exploitation. Cloudflare reported attack traffic against its customers, with attackers changing payloads to get past its rules. Neither CVE was in CISA's Known Exploited Vulnerabilities catalog as of October 21.

Step 1: find the PHP in your stack

Run this on each Docker host. It asks every running container whether it has a PHP binary and prints the version if it does:

for c in $(docker ps --format '{{.Names}}'); do
  v=$(docker exec "$c" sh -c '
    for b in php php85 php84 php83 php82 php81 php8 php7; do
      command -v "$b" >/dev/null 2>&1 && exec "$b" -n -r "echo PHP_VERSION;"
    done
    command -v frankenphp >/dev/null 2>&1 && exec frankenphp version
  ' 2>/dev/null)
  if [ -n "$v" ]; then echo "$c: $v"; fi
done

It works in bash and zsh. The list of binary names covers images that install PHP from Alpine packages, which name it php84 rather than php. Containers with no shell won't show up. That's rare for a PHP image, but check any you know run PHP by hand.

Anything below 8.5.12, 8.4.27, 8.3.36, or 8.2.35 is vulnerable. Anything on 8.1 or older won't be fixed by an upstream release at all. The script reports the command-line PHP, which in practically every image comes from the same build as the web worker.

Step 2: the apps that don't look like PHP

Nobody running WordPress, Drupal, Joomla, MediaWiki, phpMyAdmin, or Nextcloud needs to be told they run PHP. The list below is the rest: popular self-hosted apps whose names, docs, and Docker images rarely mention it. For each, the third column says where the recommended image gets its PHP, which decides who has to act before a pull fixes it (Step 3 explains the categories). Everything here was checked against each project's Dockerfiles and registry on October 21, 2026, between 15:00 and 16:20 UTC. Image details change quickly, so treat the notes as a snapshot and the Step 1 script as the source of truth for your own containers.

"LinuxServer.io" means the image is built by LinuxServer.io on Alpine Linux and gets PHP from Alpine's packages. "Official php image" means the Dockerfile starts FROM php:….

Dashboards and home-lab utilities

App What it is Where its PHP comes from
Heimdall App dashboard (Laravel) LinuxServer.io
Organizr Dashboard and single sign-on for media servers Its own image: PHP 8.1.26 on Alpine 3.18, last pushed December 2023
Speedtest Tracker Scheduled internet speed tests (Laravel) LinuxServer.io, which the project's docs recommend
LibreSpeed Self-hosted speed test Official php image by default; Alpine and LinuxServer.io variants
Wallos Subscription tracker Official php image; rebuilt when the app is updated
Grocy Groceries and household management LinuxServer.io, on Alpine 3.23 (PHP 8.5.6)

Monitoring and network

App What it is Where its PHP comes from
Zabbix Monitoring. The server is C; the web frontend container is PHP Alpine or Ubuntu packages, depending on the tag
LibreNMS Network monitoring (Laravel) Alpine packages; the current image is on Alpine 3.23 (PHP 8.4.21), and the repository moved to 3.24 on October 21
phpIPAM IP address management Alpine packages; version tags like v1.8.3 aren't rebuilt
Cacti Network graphing No official image. The most-pulled community image runs PHP 7.2 on CentOS 8, last pushed May 2021

Mail

App What it is Where its PHP comes from
mailcow Mail server suite. Its admin UI and API run in the php-fpm-mailcow container Official php:8.2-fpm-alpine3.21, a tag that stopped updating in October 2025; image pinned in compose, updated with update.sh
Roundcube Webmail Official php image (Debian) for the apache and fpm variants; fpm-alpine uses a frozen tag (PHP 8.4.15)
SnappyMail Webmail Official php image; the last published image is from October 2024 (PHP 8.2.24)
Mailu Mail server suite. Python, except the webmail container (Roundcube and SnappyMail) Alpine 3.20 packages in the current release (PHP 8.3.15)

Passwords and secrets

App What it is Where its PHP comes from
Passbolt CE Team password manager (CakePHP) Debian 13 packages; already on 8.4.27 on October 21. The sample compose file pins a digest, so update it
PrivateBin Encrypted pastebin Alpine packages; rebuilt daily
2FAuth Two-factor code manager (Laravel) Alpine packages; new images only with app releases

Money and business

App What it is Where its PHP comes from
Firefly III Personal finance (Laravel), plus its Data Importer Official php image, via serversideup/php and Firefly's own base image; the app image is rebuilt on release
Invoice Ninja Invoicing (Laravel) Official php image (invoiceninja/invoiceninja-debian). The older invoiceninja/invoiceninja image is deprecated and still runs PHP 8.2.28
Kimai Time tracking (Symfony) Official php image with Apache for :stable; LinuxServer.io variant
Akaunting Accounting (Laravel) Official php:8.1-apache (PHP 8.1.34); no image pushed since March 2026
Dolibarr ERP and CRM Official php image, rebuilt weekly; tags for Dolibarr 16 to 18 are on PHP 8.1
FreeScout Help desk nfrastack's image on Alpine packages; it already turns off session upload progress
Snipe-IT IT asset management (Laravel) Ubuntu 24.04 packages; latest was already patched on October 21
GLPI IT service management (Symfony) Official php image with Apache
Part-DB Electronic parts inventory (Symfony) Debian plus deb.sury.org packages; a FrankenPHP variant also exists

Reading, bookmarks, and wikis

App What it is Where its PHP comes from
FreshRSS RSS reader Debian 13 packages; the 1.30.3 image was already patched on October 21. Alpine and LinuxServer.io variants wait on Alpine
Tiny Tiny RSS RSS reader Alpine packages, from the community continuation at tt-rss/tt-rss. The original project retired on November 1, 2025, and its old cthulhoo/ttrss-* images are gone
Wallabag Read-it-later (Symfony) Alpine 3.19 with PHP 8.1.32; last pushed October 2025
Shaarli Bookmarks Alpine packages
LinkAce Bookmarks (Laravel) Official php image, via its own base image
RSS-Bridge Generates RSS feeds for sites without them Debian 12 packages, which Debian had already fixed; needs a rebuild
BookStack Wiki and documentation (Laravel) No official image; the docs list LinuxServer.io and solidnerd/bookstack (official php image)
DokuWiki Flat-file wiki Official php image, checked daily and rebuilt when it changes; LinuxServer.io variant

Photos, music, and media

App What it is Where its PHP comes from
AzuraCast Web radio station Official php image; the stable channel is rebuilt only on release (PHP 8.5.9)
Koel Music streaming (Laravel) Official php:8.4.8-apache-bookworm, pinned to that exact PHP version
Ampache Music streaming Debian plus deb.sury.org packages, which are already fixed; needs a rebuild
Lychee Photo gallery (Laravel) FrankenPHP, pinned by digest; LinuxServer.io variant
Piwigo Photo gallery Its own image on Alpine packages; the LinuxServer.io image is on Alpine 3.22 (PHP 8.4.16)
Castopod Podcast hosting FrankenPHP via serversideup/php; last image February 2026 (PHP 8.4.18)

Fediverse

App What it is Where its PHP comes from
Pixelfed Photo sharing (Laravel) FrankenPHP via serversideup/php; the repository's compose file builds the image locally
Friendica Social network Docker Official Image, on the official php image with Apache
Mbin Reddit-style threadiverse (Symfony) FrankenPHP; rebuilt on release

Everything else

App What it is Where its PHP comes from
Nextcloud All-in-One Nextcloud installer and manager. Its admin interface is a PHP app Official php image, pinned to exact versions, bumped to the fixed ones on October 21
Kanboard Kanban board Alpine packages
Leantime Project management (Laravel) Official php image
Baïkal CalDAV and CardDAV server Community ckulka/baikal:latest on official php:8.1-apache (PHP 8.1.34)
Monica Personal CRM (Laravel) Docker Official Image on php:8.2-apache; the app's last release was May 2024
YOURLS URL shortener Docker Official Image, on the official php image with Apache
Shlink URL shortener, on the RoadRunner app server Official php:8.5-alpine3.22, frozen at 8.5.7 since June
Matomo Web analytics Docker Official Image, on the official php image with Apache
Pterodactyl, Pelican Game server panels (Laravel). The Wings daemon on each node is Go Official php image
Adminer Database admin, often added to a compose file and forgotten Docker Official Image, running PHP's built-in web server

A few of these deserve a second look.

  • phpMyAdmin's image name. The Docker Official Image is phpmyadmin. The older phpmyadmin/phpmyadmin image, which many compose files still reference, was last pushed in October 2025 with PHP 8.3.26; its README says it is "maintained as a courtesy."
  • The Nextcloud All-in-One interface runs on ports 8080 and 8443 and has the Docker socket mounted, so code execution there amounts to control of the host. Keep those ports off the internet.
  • Images running Apache with mod_php, the setup where the advisories confirmed full exploitation of both bugs, include every official -apache variant (Nextcloud, WordPress, Matomo, Friendica, YOURLS, Monica), plus Akaunting, Dolibarr, Kimai's :stable, GLPI, DokuWiki, FreshRSS, and Snipe-IT.
  • Shlink and Adminer don't run PHP the usual way (RoadRunner and PHP's built-in server). The advisories don't address either setup, so treat both as affected.

Not in a container, but PHP too. The web interfaces of Unraid, pfSense, and OPNsense are PHP applications. They get PHP from their vendors' own updates, so watch for those, and keep the admin pages off the internet in any case.

No longer PHP. Pi-hole v6 dropped PHP: its web interface is now served by Pi-hole's own C daemon. Pi-hole v5 still ran lighttpd and PHP. ownCloud Infinite Scale and Pydio Cells are written in Go; classic ownCloud Server is still PHP.

Step 3: why pulling may not fix it yet

A PHP fix reaches your container in several hops: the PHP release, then whatever the image is built on, then the app's own image, then your pull. Each hop has a different maintainer, and the image is only as current as the slowest of them. Every self-hosted image gets its PHP in one of three ways.

Built on Docker's official php image. Most project-built images start from a line like FROM php:8.4-apache. So do Docker's own official images for apps, such as nextcloud, wordpress, matomo, friendica, yourls, monica, phpmyadmin, and adminer. Docker's maintainers merged the PHP update into the official image definitions at 15:16 UTC on October 21. At 16:00 UTC, the patched tags were listed on Docker Hub but couldn't be pulled yet, and floating tags such as php:8.4-apache still served 8.4.26. Docker rebuilds its official app images when their base image changes, so those should follow on their own. Images built by the projects themselves follow whenever the project's build next runs. DokuWiki's checks the base image daily, and Roundcube's and Pelican's rebuild weekly. Many rebuild only when the app has a release, which can be weeks or months away. Timing matters too: LinkAce rebuilt its base image at 15:59 UTC on October 21 and still got PHP 8.5.11, because Docker's patched tags weren't live yet.

PHP from Alpine or Debian packages. Every LinuxServer.io PHP image works this way, as do the images for FreshRSS, Snipe-IT, Passbolt, Kanboard, PrivateBin, phpIPAM, 2FAuth, LibreNMS, Shaarli, Tiny Tiny RSS, and Zabbix's web frontend. These wait on the distribution first. Debian and Ubuntu moved fast: by the afternoon of October 21, Debian 12 and 13 and Ubuntu 22.04 and 24.04 all had fixed PHP packages, which is why the Passbolt, FreshRSS, and Snipe-IT images were already patched. Ondřej Surý's widely used PHP repository for Debian and Ubuntu, deb.sury.org, had all four fixed versions out by 13:36 UTC, and also backported the fix to its PHP 8.1 package. Alpine had not shipped yet: at 16:00 UTC, its 3.24 and edge repositories still had 8.5.11, 8.4.26, and 8.3.35. Once Alpine ships, images still need a rebuild. PrivateBin's images are rebuilt daily, and LinuxServer.io's when package versions change.

FrankenPHP. Lychee, Mbin, Castopod, and Pixelfed run on FrankenPHP, an app server with PHP compiled in, whose images are themselves built on the official php image. That adds a hop: Castopod and Pixelfed go official php image, then FrankenPHP, then the serversideup/php base, then the app.

Five ways a rebuild doesn't help

Some images will stay vulnerable even after the PHP release, the base image update, and a fresh build. As of October 21:

  1. The image is built on an old Alpine release. Alpine's older branches had stopped getting new PHP point releases: as of October 21, Alpine 3.23 still had PHP 8.5.6 from May, 3.22 had 8.4.16 from December 2025, and 3.20 had 8.3.15 from December 2024. Rebuilding on those branches ships the same old PHP. That catches LinuxServer.io's Grocy (Alpine 3.23, PHP 8.5.6), LinuxServer.io's Piwigo (3.22, PHP 8.4.16), and the webmail container in Mailu's current release (3.20, PHP 8.3.15).
  2. The image uses an official php tag that's no longer updated. Official tags tied to an old Alpine release stop moving when Docker drops that release. mailcow's PHP container is built on php:8.2-fpm-alpine3.21, last updated in October 2025; a pending change moves it to a current base. Roundcube's fpm-alpine variant uses php:8.4-fpm-alpine3.21 (PHP 8.4.15). Shlink uses php:8.5-alpine3.22, frozen at 8.5.7 since June. The deprecated invoiceninja/invoiceninja image, rebuilt as recently as October 18, uses php:8.2-fpm-alpine3.20 and ships PHP 8.2.28.
  3. The Dockerfile pins an exact PHP version. Koel's image is built on php:8.4.8-apache-bookworm, so it stays on PHP 8.4.8 until someone edits the line. Nextcloud All-in-One pins exact versions too, but it bumped them to 8.5.12 and 8.4.27 on October 21 and is waiting on Docker's images.
  4. Your compose file pins the image. mailcow pins its image tags in its compose file and updates through its own update.sh. Passbolt's sample compose file pins a digest. phpIPAM's version tags, like v1.8.3, aren't rebuilt. Pixelfed's compose file builds the image locally, so pull does nothing and the fix needs docker compose build --pull.
  5. It's on PHP 8.1 or older. No official PHP release will fix it. Ubuntu 22.04 and deb.sury.org backported the fix to their PHP 8.1 packages, but the official php:8.1 images were last built at the turn of the year and won't be rebuilt. Akaunting's image runs the official php:8.1-apache and hasn't been pushed since March 2026. Wallabag's runs PHP 8.1.32 on Alpine 3.19, last pushed in October 2025. Organizr's runs PHP 8.1.26 on Alpine 3.18, last pushed in December 2023. ckulka/baikal:latest, the community image Baïkal's own docs point to, and Dolibarr's tags for versions 16 to 18 are built on official 8.1 images, so their weekly rebuilds re-ship the same PHP. The most-pulled community Cacti image runs PHP 7.2 on CentOS 8 and hasn't been pushed since May 2021.

The Baïkal and Piwigo cases are the warning: those images were rebuilt within the past week and still carry old PHP. A recent push date says nothing about the PHP inside. Run the script from Step 1 after every pull and look at the version.

Until your image is patched

The PHP advisories say plainly that updating is the only complete fix. Until a rebuilt image exists, the goal is to keep anonymous requests away from PHP, and to change a few settings that narrow the attack.

Take it off the internet. An app reachable only on your LAN, or only over a VPN such as Tailscale or WireGuard, can only be attacked by someone already inside. For most homelab apps this is the cheapest stopgap and the most complete one.

Put authentication in front of PHP, not inside it. The app's own login page doesn't help, because PHP processes the request before the app's code runs. Forward authentication at the reverse proxy does help: Authelia, Authentik, oauth2-proxy, or Cloudflare Access turn anonymous requests away before they reach the container. A plain reverse proxy with no authentication does nothing here; it forwards the malicious upload as-is. The catch is the bypass rules. Share links, CalDAV and CardDAV, mobile-app APIs, and webhooks often skip forward auth because their clients can't log in through it, and those paths stay exposed.

Cloudflare's WAF, if you already use it. Since 03:02 UTC on October 21, Cloudflare's managed rules block known PHP2Shell attempts on every plan with the WAF deployed, including Free. That covers hostnames proxied through Cloudflare, and Cloudflare Tunnel hostnames inherit the zone's WAF rules. Cloudflare calls it a stopgap, and it says attackers were already mutating payloads to get around it.

PHP settings that narrow it. The advisories list several, all to be set at the administrator level (a php.ini or conf.d file, or php_admin_value in an FPM pool) and followed by a restart of every PHP worker:

  • session.upload_progress.enabled=0 removes a trigger that CVE-2026-109619 uses. The advisory suggests leaving it off even after patching if the app doesn't need upload progress.
  • fatal_error_backtraces=0, on PHP 8.5 only, removes the confirmed trigger for the same bug.
  • memory_limit=-1 is the advisory's main configuration mitigation for CVE-2026-109618, and it is not complete. It also removes PHP's own memory cap, so a runaway request can use whatever RAM the container allows. Check that the app doesn't set its own limit back through an environment variable or per-directory config.
  • pm.max_requests=1 in a PHP-FPM pool makes each worker exit after one request, so no state carries over to the next. It costs performance, which on a single-user homelab app may not matter.

To add an ini file, mount it into the directory your image reads. Images built on the official php image read /usr/local/etc/php/conf.d/. LinuxServer.io's PHP images load any .ini file in /config/php/, which is already on your volume.

What the patch says about your images

A security release like this one is the cleanest test of how an image is maintained, because it asks every publisher the same question on the same day. By the afternoon of October 21 (UTC), the images that were already patched had something in common: they build on Debian or Ubuntu packages and rebuild when those packages change. The images at risk of staying vulnerable share the opposite traits: a base pinned to an exact PHP version or to an old distribution release, or a build that only runs when someone cuts an app release.

That's worth knowing the next time you pick an image. Many of these apps have two or three to choose from (the project's own, LinuxServer.io's, a community build), and they don't age the same way. Look at the FROM line and at what triggers a rebuild, not only at the app version on the tag.