Skip to content
Engineering7 min read

PHP2Shell: the ACSC's PHP alert, and how I patched our Docker images

The ACSC put out a critical alert for two bugs in PHP itself. Here's what they actually do in plain language, who needs to care, and the exact one-line Dockerfile change I shipped to fix it.

Last week the Australian Cyber Security Centre put out a critical alert about two vulnerabilities in PHP. Not Magento, not Laravel, not WordPress. PHP itself. They're tracked as CVE-2026-109618 and CVE-2026-109619, people have started calling the pair PHP2Shell, and under the right conditions they let someone run code on your server without logging in. If your site runs on PHP (and every Magento, Adobe Commerce, Laravel and WordPress site does) this one is worth ten minutes of your time. The good news is the fix is boring: update PHP, restart it, done. No Magento patch or Composer update covers this, because the bug isn't in your application code.

What the bug actually is

A couple of clients have asked me about "the memory limit and caching thing". That's a fair summary, so here's what's going on underneath. Every PHP request gets a memory budget, the `memory_limit` setting. On a Magento box that's usually somewhere between 756M and 2G. When a request goes over it, PHP doesn't finish what it was doing. It stops dead, right there, and jumps straight to cleanup. The first bug (CVE-2026-109618) is that some of PHP's own internals can be halfway through setting something up when that happens. Cleanup then treats the half-built data as if it were finished. If an attacker can make PHP run out of memory at exactly the right moment, say with a carefully shaped form upload, they can turn that into memory corruption and then into running their own code. The second bug (CVE-2026-109619) is about leftovers. A PHP-FPM worker handles lots of requests one after the other. Some internal state can survive from one request into the next while still pointing at memory that's already been freed. The next request on that worker picks it up. The confirmed way in is PHP's session upload progress feature, which is on by default and runs before your code gets a look in. Now the caching part. The researchers at Wiz showed that running out of memory while OPcache is saving compiled code into shared memory can leave OPcache's shared data in a broken state, and they turned that into code execution as well. So yes, memory limits and caching are both in the picture. But the root cause is how PHP handles running out of memory and tidying up between requests. It is not something you misconfigured. The way I've been explaining it: a kitchen hand gets pulled off a plate halfway through, and the next person just sends out whatever is sitting on the bench without looking at it.

Who needs to care

Every PHP release before these versions:

  • PHP 8.5 before 8.5.12
  • PHP 8.4 before 8.4.27
  • PHP 8.3 before 8.3.36
  • PHP 8.2 before 8.2.35

PHP 8.1 and older are end of life and got no official release. The PHP team published source patches for 7.4, 8.0 and 8.1, and some Linux distros have backported the fix, but if you're on those versions this is your sign to upgrade properly. How you run PHP doesn't get you off the hook. PHP-FPM behind Nginx, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed are all on PHP's list. mod_php is rated the highest risk because the attack lands before any of your code runs. FPM is harder to exploit, but PHP confirmed an attack path against it, so don't treat it as safe. People are actively trying. Amazon's threat intelligence team saw an exploitation attempt on 17 October, four days before the patch came out. Whether those attempts actually work yet is less clear. The PHP Foundation looked at the leaked exploits and didn't find a working one at release time. I wouldn't bet a store on that holding. For Magento and Adobe Commerce: 2.4.9 runs on PHP 8.5 and 2.4.8 runs on 8.3 or 8.4, so current stores have a patched version waiting. If you're on an older 2.4.x stuck on PHP 8.1, there's no clean fix for you and the platform upgrade can't wait any longer. For Laravel: most apps I look after are on 8.3 or 8.4. Check what the server actually runs, not what `composer.json` says it should.

Check what you're running

On the server:

bash
php -v
php-fpm -v   # or php-fpm8.3 -v on Debian/Ubuntu

The command line PHP and the FPM PHP aren't always the same build, so check both. In Docker:

bash
docker compose exec php php -v

One catch with Debian and Ubuntu: they backport fixes without changing the version number. A patched Ubuntu 24.04 box still says PHP 8.3.6. What you actually want is the package version, so check `dpkg -l | grep php8.3-fpm` and look for `8.3.6-0ubuntu0.24.04.13` or later.

How I fixed it in Docker

Our PHP-FPM image for Magento is built on the official `php:*-fpm` image (Debian based), with the extensions Magento needs on top. The fix was one line in the Dockerfile. Before:

dockerfile
FROM php:8.4-fpm

After:

dockerfile
FROM php:8.4.27-fpm

That looks too small to matter, so here's why it matters. The floating `8.4-fpm` tag does point at 8.4.27 now, but only if you pull it again. Docker will quite happily keep building from the old 8.4.26 layers it already has cached on the build machine. I've been bitten by that before. Pinning the exact patch version means the change shows up in git, it shows up in code review, and a rebuild can't silently use the old base. The rest of the Dockerfile didn't change. Same `useradd`, same `apt-get` list, same `docker-php-ext-install` block, same ini files. I also added a small build script at the same time, because I'd been typing the buildx command by hand for too long:

bash
#!/bin/sh
# Usage: ./build.sh <image[:tag]> [extra buildx args, e.g. --push]
set -eu
IMAGE="${1:?usage: ./build.sh <image[:tag]> [buildx args...]}"
shift
PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}"
BUILDER=phpfpm-multiarch

docker buildx inspect "$BUILDER" >/dev/null 2>&1 \
  || docker buildx create --name "$BUILDER" --driver docker-container >/dev/null

docker buildx build --builder "$BUILDER" --platform "$PLATFORMS" --pull -t "$IMAGE" "$@" .

The important flag is `--pull`. It forces Docker to fetch the base image fresh instead of trusting what's cached. Building for both amd64 and arm64 in one go means the same image runs on the Apple Silicon laptops we develop on and the x86 servers we deploy to. Then it's the usual rebuild and redeploy:

bash
./build.sh registry.example.com/phpfpm:8.4.27 --push
docker compose pull php
docker compose up -d php
docker compose exec php php -v

You want to see `PHP 8.4.27` come back. `up -d` recreates the container, so every FPM worker restarts on the new build. There's no sense patching PHP and leaving the old workers running. If you're not on 8.4, it's the same change with `8.3.36-fpm`, `8.2.35-fpm` or `8.5.12-fpm`.

What else I'd do

  • If you can't patch today, make sure the site is behind a WAF. Cloudflare pushed rules for this to both free and paid plans on 21 October. That's a stopgap, not a fix, and PHP is clear that no firewall rule fully protects you.
  • Have a look through your PHP-FPM logs for "Allowed memory size exhausted" errors sitting right after file upload requests, bursts of 500s, or workers restarting for no obvious reason.
  • If a server was exposed and unpatched for a while, rotate the secrets it holds. Database passwords and API keys in `env.php` or `.env` are the obvious ones.
  • Stay on a supported PHP branch. 8.2 stops getting security fixes on 31 December 2026. If you're on it, plan the move to 8.4 or 8.5 now rather than in December.
  • Pin your base images to exact versions and rebuild on a schedule, not just when something breaks. A weekly build with `--pull`, plus Renovate or Dependabot watching the Docker tags, catches most of this for you.

The short version

  • Two bugs in PHP itself, both fixed in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Nothing older gets an official fix.
  • They work by pushing a request over `memory_limit` at the wrong moment, so PHP cleans up half-finished data or reuses freed memory on the next request. OPcache's shared memory is one of the ways in.
  • Attackers were already trying before the patch came out.
  • In Docker the fix is pinning the base image to the patched version, rebuilding with `--pull`, and recreating the container so the workers restart.
  • Patch, then check the version inside the running container, not on your laptop.

If you run a Magento or Laravel site and you're not sure what version you're actually on, or you'd like someone to look over your hosting setup while this plays out, get in touch and I'll tell you where you stand.

PHPSecurityDockerMagentoLaravelPHP2Shell
Work with me

Got a project like this?

Tell me what’s slow, broken or stuck. You’ll get a straight answer within one working day.

Keep reading · Next in the blogShopify smart collections, for MagentoMagento5 min read

More in Engineering

All posts →