PHP2Shell: patching a custom Nextcloud Docker image (and the :latest trap)

After patching my WordPress stack for PHP2Shell (CVE-2026-109618 and CVE-2026-109619), the next one on the list was Nextcloud. It deserved more care: it holds my files, and it runs on the official nextcloud:apache image, which means Apache with mod_php, the setup where the confirmed exploit fires before any application code runs.

The setup

Nextcloud runs as a Docker Compose stack inside a Proxmox LXC: MariaDB, Redis, the Nextcloud app container and a cron container. TLS is terminated on a separate Nginx Proxy Manager host. The app image is a small custom build on top of the official one, because I need the PHP smbclient extension for SMB external storage:

FROM nextcloud:apache

RUN apt-get update \
 && apt-get install -y --no-install-recommends libsmbclient-dev smbclient \
 && pecl install smbclient \
 && docker-php-ext-enable smbclient \
 && rm -rf /var/lib/apt/lists/*

Because it’s a custom image, a simple docker compose pull does nothing for the app. The image has to be rebuilt on a patched base. Until then, the proxy host stayed disabled.

The trap: nextcloud:apache is effectively :latest

My Dockerfile even had a comment saying “pin the major”, but the tag didn’t do it. nextcloud:apache follows the newest Nextcloud release. Rebuilding with --pull during an emergency could have dragged in a new major version, and the container upgrades itself on start. Not something you want in the middle of a security patch.

So first: find the running major, then pin to it.

docker compose exec -u www-data app php occ status
#  - versionstring: 34.0.4

sed -i 's|^FROM nextcloud:apache|FROM nextcloud:34-apache|' Dockerfile

34-apache still follows 34.x maintenance releases, but never jumps to 35.

Check the base image before building

Pull the base image and look at the PHP version inside it. The fixed releases are 8.5.12, 8.4.27, 8.3.36 and 8.2.35:

docker pull nextcloud:34-apache
docker run --rm --entrypoint php nextcloud:34-apache -r 'echo PHP_VERSION, PHP_EOL;'
# 8.5.12

docker compose exec app php -v
# PHP 8.5.10 (currently running)

Comparing both also tells you whether you’re about to jump a PHP minor version. Here it was 8.5.10 to 8.5.12, a patch-level update, so the pecl-compiled extension wasn’t going to cause surprises. (It also showed the container had already missed the September security release. Floating tags only help if you actually rebuild.)

Snapshot, rebuild, restart

App code, data and database live on one ZFS dataset, so a single snapshot is a consistent rollback point. Then rebuild without cache, so Docker can’t reuse the old layers:

zfs snapshot <dataset>@pre-php2shell   # on the Proxmox host

docker compose build --pull --no-cache app
docker compose up -d app cron

docker compose exec app php -v
# PHP 8.5.12 (cli) (built: Oct 21 2026 15:49:40)
docker compose exec cron php -v
# PHP 8.5.12
docker compose exec app php -m | grep -i smbclient
# smbclient

Both containers on the fixed PHP, and the SMB extension still loads, so external storage keeps working.

A small surprise: 34.0.4 became 34.0.5

After the restart, occ status reported 34.0.5. That’s the pin working as intended: the tag tracks the latest 34.x, and the upgrade ran cleanly on start (no maintenance mode, no pending DB upgrade). After any version bump I run the usual follow-ups:

docker compose exec -u www-data app php occ db:add-missing-indices
docker compose exec -u www-data app php occ app:update --all

Compromise checks before going back online

Exploitation attempts were reported right after the leak, so before re-enabling the proxy host:

# no output = no modified or unexpected core files
docker compose exec -u www-data app php occ integrity:check-core

# only accounts I know should be admins
docker compose exec -u www-data app php occ group:list --output=json

# when did each admin last log in?
docker compose exec -u www-data app php occ user:lastseen <user>

# the Collabora built-in CODE app had its own exploited bug last week
docker compose exec -u www-data app php occ app:list | grep -i richdocumentscode

All clean, so Nextcloud is back online.

Takeaways

  • Custom images don’t patch themselves. A pull updates the base, but your image only changes when you rebuild it with --pull --no-cache.
  • Pin the major, float the minor. nextcloud:34-apache gets security and maintenance releases without surprise major upgrades.
  • Check the PHP version inside the image before recreating anything, and again inside the running containers afterwards.
  • mod_php is the riskiest variant for this class of bug. Moving to nextcloud:34-fpm behind nginx is on my list, as a separate, planned change.

Sources

Leave a comment