# PHP2Shell in Docker: Two PHP Runtimes Nobody Listed

- Author: Abdullah Chaudary
- Category: Infrastructure
- Published: 2026-10-22T17:07:48.599Z
- Updated: 2026-10-22T17:07:48.629Z
- Reading time: 9 min
- Tags: PHP, Docker, CVE, Patch Management, mailcow, Container Security

> PHP2Shell is fixed in the PHP runtime, so a Docker image needs a newer PHP such as 8.3.36, not an app update. I found PHP inside mailcow and an LMS on my host, and patched both.

I don't run PHP. That's what I'd have told you on Wednesday, when PHP shipped 8.5.12, 8.4.27, 8.3.36 and 8.2.35 to fix PHP2Shell, two critical remote code execution bugs. On Thursday morning I ran `which php php-fpm` on one of my production hosts just to be sure, and it printed nothing.

So I ran `php -v` inside every container. Two returned a version. The mail server was running PHP 8.2.29. An LMS on the same host was running 8.3.35. Both were on the internet, and I'd never thought of either one as "PHP".

Patching them took about five minutes. The uncomfortable part came later, when I worked out that the log check I'd run that morning couldn't have found anything even if something had happened.

**TL;DR:** PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is fixed in the PHP runtime, so updating your app does nothing. If PHP lives inside a Docker image, you need a newer image. Run `php` in every container to find them, rebuild or re-pull, and check the version and build date on the running binary, not the tag. Then check PHP is actually logging errors before you trust a clean grep, because the official images don't.

## Is my Docker PHP image affected by PHP2Shell?

If the PHP inside the image serves web traffic and it's older than 8.5.12, 8.4.27, 8.3.36 or 8.2.35, yes. The bug is in PHP itself, so it doesn't matter what app runs on top. php.net's advisory from 21 October just says: "All PHP users are strongly encouraged to update immediately."

There are two advisories behind it. GHSA-w8hw-w9fx-j79w (CVE-2026-109618) is about how PHP handles running out of memory. GHSA-6wmf-4rv5-p3rp (CVE-2026-109619) is about state a worker keeps between requests.

The advisories are careful about what's actually been exploited. For the memory bug, full RCE "was validated over HTTP against mod\_php with default PHP input and memory limits", and "no complete FPM exploit has been demonstrated for this SAPI". The session bug "was validated against mod\_php and FPM". I didn't wait to find out whether my setup was the easy case.

## How I found the PHP I didn't know I had

I looped over `docker ps` and ran this in each container:

```bash
docker exec <container> php -r 'echo PHP_VERSION;'
```

Most containers just errored. Two printed a version:

- **The LMS.** Chamilo 3.0.1 in an image built on 4 October `FROM php:8.3-apache`. PHP 8.3.35.
- **mailcow's ****php-fpm-mailcow**** container.** Image `ghcr.io/mailcow/phpfpm:8.2.29-1`, built `Oct 8 2025`. PHP 8.2.29.

Both had a public vhost in the host's nginx config. One container had no shell at all, so I couldn't exec into it, and I skipped it.

Nobody picked either of those PHP versions. They're whatever was inside the base image on the day it was built or pulled, and nothing on the host was ever going to update them. That's the part I'd underestimated.

Chamilo itself isn't the problem, by the way. 3.0.1 was its own security release for 3.0.0, out on 19 September, and has nothing to do with PHP2Shell. The PHP comes from Docker's `php` image underneath it.

## Is mod\_php or PHP-FPM more at risk?

mod\_php. php.net puts it plainly: "We consider the risk highest for mod\_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs." FPM is still in scope.

I had one of each. The LMS container has `php.load` in `/etc/apache2/mods-enabled`, which is mod\_php (Docker Hub describes the `-apache` tags as Apache "in conjunction with PHP (as `mod_php`)"). In mailcow's container, `php-fpm -v` printed `PHP 8.2.29 (fpm-fcgi)`.

## Don't trust `php -v` for your web settings

`php -v` and `php -i` report the command-line PHP. Web requests go through Apache's module or `php-fpm`, which can have different settings. On the LMS, the CLI reported `memory_limit 128M` while Apache sets `php_admin_value memory_limit 256M` for web requests.

That matters for the session bug. The CLI reported `session.upload_progress.enabled On` in both containers. To get the web-side value I grepped mailcow's `data/conf/phpfpm/`, the image's `php-fpm.d`, and the LMS's `conf.d` and Apache config. Nothing overrode it, so web requests got the default, which is On. The advisory notes: "Session upload progress is enabled by default, and no application call to `session_start()` is required."

## Patching the LMS: rebuild with `--pull`

I tagged the current image so I could roll back, then rebuilt:

```bash
docker tag <lms-image>:latest <lms-image>:pre-php2shell
docker compose build --pull app
docker compose up -d app
```

`--pull` makes Docker go and look for a newer `php:8.3-apache` instead of using the one it has cached. Without it you can rebuild and ship exactly the same PHP, the same kind of surprise as [a Docker build that succeeds and still ships old content](/blog/nextjs-stale-page-after-deploy-docker-layer-cache). Chamilo is pinned with `ARG CHAMILO_VERSION=3.0.1`, so only PHP moved.

## Patching mailcow, and the Compose error that scared me

For mailcow I only wanted to swap the php-fpm image. I backed up `docker-compose.yml`, changed the tag from `8.2.29-1` to `8.2.35-1` with `sed`, pulled it, and ran `docker compose up -d php-fpm-mailcow`. It failed:

```text
failed to create network mailcowdockerized_mailcow-network: ... Pool overlaps with other one on this address space
```

For a second I thought I'd taken the mail server down. I hadn't. The old container was still serving 8.2.29, and the web UI and SOGo both still returned 200.

The problem was the project name. This stack runs as Compose project `mailcow`, but `mailcow.conf` (which is symlinked as `.env`) says `COMPOSE_PROJECT_NAME=mailcowdockerized`. Compose picked up the name from `.env`, treated it as a separate project, and tried to create a second network on the same address range.

Passing the real project name with `-p` fixed it:

```bash
docker compose -p mailcow ps -q                            # matched all 18 services
docker compose -p mailcow up -d --no-deps php-fpm-mailcow  # recreates just the one container
```

If your mailcow was set up a while ago, check which project name it's really running under before you copy commands from the internet. mailcow's issue #7538 suggests verifying with `docker exec mailcowdockerized-php-fpm-mailcow-1 php -v`, and on my box that container doesn't exist.

## How do I verify the PHP in a container is patched?

Run `php -v` inside the recreated container and read the version and the `built:` date. Compare against 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Check the container that's actually serving requests, not the image sitting on disk.

Don't trust the tag. That same issue #7538 is about mailcow's `8.2.35-1` image, pushed around 15:42 UTC on 21 October, which actually contained PHP 8.2.34. It was re-pushed with a real 8.2.35 build around 17:25 UTC. The reporter thinks a rolling `php:8.2-fpm-alpine3.21` base tag was still pointing at 8.2.34. I pulled the next morning and got the good build, but had I pulled that afternoon, I'd have had a `8.2.35-1` tag running a PHP 8.2.34 binary.

Here's what I recorded, all UTC on 22 October:

```text
LMS before   09:48:20  PHP 8.3.35 (cli) (built: Sep 25 2026 17:43:09)
LMS after    09:51:28  PHP 8.3.36 (cli) (built: Oct 21 2026 15:57:18)
mailcow before         PHP 8.2.29 (fpm-fcgi)
mailcow after 09:53:01 PHP 8.2.35 (fpm-fcgi) (built: Oct 21 2026 16:05:36)
```

After each change I checked the sites were up: the LMS returned 200, mailcow's UI and SOGo returned 200, all mailcow services were Up and the new container logged no errors. Pulling an image does nothing on its own. The container has to be recreated, much like [a patched kernel that's installed but not booted](/blog/linux-kernel-cve-patching-vps-vs-dedicated).

## The log check that couldn't find anything

Before patching I wanted to know if anyone had already tried this on me. The write-ups point at one log line. Datadog Security Labs shows it from their own exploit attempt:

```text
PHP Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 3500032 bytes) in Unknown on line 0
```

So I grepped `docker logs --since 2026-10-15` on both containers for `Allowed memory size`, `Segmentation fault`, `exit signal` and `in Unknown on line 0`. Zero matches. `docker top` showed only apache2 and php-fpm. I wrote down "no indicators found" and moved on.

Later I built a small lab to reproduce the line, and it didn't appear until I set `log_errors=On`. That sent me back to production. The official `php` images ship `php.ini-production` and `php.ini-development` but load neither. In both of my containers, `php -i` showed `Loaded Configuration File => (none)`, `log_errors => Off` and `error_log => no value`. mailcow's pool file even has the setting, commented out: `;php_admin_flag[log_errors] = on`.

So PHP was never going to write that error anywhere. My zero didn't mean nobody tried. It meant I'd grepped for a line that couldn't exist. Apache and php-fpm would still have logged a crashed worker, so that half of the check was real. The PHP error half wasn't.

## What does "Allowed memory size ... in Unknown on line 0" mean?

It means PHP ran out of memory while setting up a request, before any of your code ran. Datadog says "PHP failed during request initialization, before any script started running". A big, harmless request can cause it too, so finding it is a lead, not proof.

I checked that in the lab. I ran `php:8.3.35-apache` (vulnerable) next to `php:8.3.36-apache` (fixed), both mod\_php under Apache 2.4.69 with `memory_limit=16M` and logging on, and sent each an ordinary 3.5 MB form POST (3,500,002 bytes). Both returned a 500 and logged the same thing:

```text
8.3.35 (vulnerable): ... (tried to allocate 3500032 bytes) in Unknown on line 0
8.3.36 (fixed):      ... (tried to allocate 3502080 bytes) in Unknown on line 0
```

The patched one logs it too, because a 3.5 MB request against a 16M limit runs out of memory either way. It never touches the bug. If you want to grep for it once logging works:

```bash
grep -E 'Allowed memory size of .* bytes exhausted .* in Unknown on line 0' <your-php-error-log>
```

Datadog also warns that "A successful exploit doesn't necessarily crash the worker or return an error." So even with logging on, a quiet log isn't an all-clear.

## Can I use the ini mitigations instead of updating?

No. php.net says they "reduce exposure to specific attack paths but do not make an installation safe". Update. I didn't apply them in production because I could just patch.

I did try them in the lab, on the vulnerable 8.3.35 container: `memory_limit=-1`, `post_max_size=8M` and `session.upload_progress.enabled=0`, set in a mounted ini file (not `ini_set()` or `.user.ini`, which the advisory says not to rely on), then `apache2ctl -k restart`. They took effect. The same 3.5 MB request now returned 200 and logged nothing. A 9 MB request got a warning instead:

```text
PHP Warning:  PHP Request Startup: POST Content-Length of 9000002 bytes exceeds the limit of 8388608 bytes in Unknown on line 0
```

Two things to notice. `memory_limit=-1` makes the out-of-memory line disappear, so it hides the very thing you'd grep for. And this warning ends in `in Unknown on line 0` as well, so don't grep for that suffix on its own.

## What I still haven't done

- **PHP error logging is still off** in both containers. Turning it on changes the config of a running app that isn't only mine, so I'm doing it deliberately, not as part of the patch. Until then, I can't say whether anyone probed these.
- **I didn't check the nginx access logs.** The log format on this host doesn't record which vhost a request hit, so grepping them would have been close to useless.
- **mailcow isn't fully updated.** I only swapped the php-fpm image. mailcow's own 2026-10 release ("Security Update: PHP 8.2.35, SOGo 5.12.12 & Nginx 1.30.6") isn't applied, and my checkout is from 29 January. That's next.
- **PHP 8.2 ends on 31 December 2026**, according to php.net, and mailcow's php-fpm is on 8.2.
- **Upload progress is still on.** The advisory says it "might be prudent" to turn it off if you don't use it, even after patching. Separate decision.
- **The sweep covered one host** and skipped the container with no shell. The lab sent one harmless request. I didn't write or run an exploit anywhere.

## References

All read on 22 October 2026.

- PHP, [Security advisory: Critical remote code execution vulnerabilities, update now](https://www.php.net/archive/2026.php#2026-10-21-5), 21 October 2026, and the PHP 8.2.35 release entry of the same date on the same page.
- php-src, [GHSA-w8hw-w9fx-j79w (CVE-2026-109618)](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w).
- php-src, [GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp).
- Datadog Security Labs, [PHP2Shell: Critical vulnerabilities in PHP (CVE-2026-109618, CVE-2026-109619): detection, analysis, and remediation](https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/).
- mailcow-dockerized, [issue #7538, `phpfpm:8.2.35-1` contained PHP 8.2.34](https://github.com/mailcow/mailcow-dockerized/issues/7538).
- mailcow-dockerized, [release 2026-10](https://github.com/mailcow/mailcow-dockerized/releases/tag/2026-10), published 21 October 2026.
- Docker Hub, [official `php` image](https://hub.docker.com/_/php).
- Docker, [`docker compose build` reference](https://docs.docker.com/reference/cli/docker/compose/build/).
- Chamilo, [release v3.0.1](https://github.com/chamilo/chamilo-lms/releases/tag/v3.0.1), published 19 September 2026.

More in [Infrastructure field notes](/blog/category/infrastructure).

If you run the stock `php` image, go and check `log_errors` now. Mine had been off the whole time, and I only caught it because my lab logged nothing until I switched it on.
