21 October 2026

📌 Wednesday night in Gourock, Necropolitan by Green Lung in the headphones, the kind of doom that sounds like it was recorded in a crypt with good acoustics. Fitting. Today the whole PHP world had its own little funeral rehearsal, and I spent the day making sure the Bottega wasn't among the mourners.

Short version, for the people who just want the facts: two critical remote code execution bugs in PHP, CVE-2026-109618 and CVE-2026-109619, CVSS v4.0 9.2 each, details leaked a day early, patch released on 21 October at 14:00 UTC. Fixed versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Nothing for 8.1 and older. If you run PHP anywhere, go update, then come back.

The rest is what the day looked like from the chair of someone who looks after a WordPress blog with twenty-five years of archive, a few PHP services on a second box, and no team.

the leak

Someone posted on Hacker News at 15:20 UTC on 20 October that two PHP RCEs were about to drop. By 19:33 the PHP security team had confirmed it on php.announce: security release scheduled for the next day, two critical vulnerabilities, "we reserve the right to release earlier". Sansec called it PHP2Shell after a leaked screenshot and predicted mass attacks within fifteen minutes of the release. A PHP core developer, asked whether turning off session upload progress was enough, answered that the only mitigation was patching, and that she sadly knew too much.

What was circulating: the bugs sit in how PHP parses incoming requests, before any application code runs. No upload form on your site doesn't mean no exposure. The request is parsed anyway.

I read all this on Wednesday morning with coffee. The patch was seven hours away. The Bottega runs WordPress on PHP 8.3 inside Docker, and Docker images don't patch themselves.

before the patch

The widely shared stopgap was to refuse every request with a multipart body at the web server, before it reaches PHP. Cost: file uploads stop working. For a blog, that means the editors can't add images for a few hours. Acceptable. I wrote to the editorial list first, then added one line to the nginx config in front of PHP-FPM:

if ($http_content_type ~* "multipart") { return 403; }

Backup of the file, sed, diff, nginx -t inside the container: syntax ok. Restart. And here is the first thing I got wrong today: that nginx -t had tested the old file. The config is a single-file bind mount, sed -i writes a new inode, the container kept seeing the previous one until the restart. The syntax check was theatre. It passed anyway, but not because I had checked anything.

Second thing: testing from outside with curl -F tells you nothing when Anubis sits in front, because Anubis answers first with its challenge page. The real test runs from the proxy container straight to nginx:

GET normale: 200
multipart POST: 403
multipart GET: 403

Committed, with the word "temporaneo" in the message so future me doesn't wonder.

the second box

The lab server runs a dozen containers. Only two are PHP: the editorial kanban and a webmail front end I use alone. The kanban got the same multipart guard in the reverse proxy; attachments blocked, everything else works, nobody noticed.

The webmail was another story:

PHP 8.1.30 (cli) (built: Sep 27 2024 00:35:36) (NTS)
PRETTY_NAME="Alpine Linux v3.20"

PHP 8.1 is out of support. No fix was coming for that image, ever. The mailbox itself lives elsewhere, the webmail was just a window on it, so the window got closed: proxy host disabled, container left running in the dark. It will come back on a current release, behind an access list, and not before. Reading mail through the provider's own interface for a while costs nothing.

Then a check I should do more often: dpkg -l 'php*' on all three hosts. Nothing. PHP lives only in containers here, and only the proxy reaches them. Good to know for sure instead of assuming.

waiting for docker hub

14:00 UTC came and went. php.net published 8.3.36. The PHP Docker image is built from php.net sources, so the chain is: PHP releases, docker-library/php merges a PR, official-images merges a PR, Docker Hub builds php:8.3-fpm, then builds wordpress:php8.3-fpm on top. Both PRs were merged within the hour. The images were not there.

$ docker pull wordpress:php8.3-fpm >/dev/null && docker run --rm wordpress:php8.3-fpm php -v | head -1
PHP 8.3.35 (cli) (built: Oct 21 2026 00:33:17) (NTS)

Built at 00:33 that morning, fourteen hours before the fix existed. I pulled again. And again. Hetzner's status page, meanwhile, said they had patched their managed hosting and that cloud customers would get an email. Fair: the cloud box is mine. Several hours of refresh later:

2026-10-21T16:30:08.462754044Z
PHP 8.3.36 (cli) (built: Oct 21 2026 15:59:09) (NTS)

If you self-host on official images, this is the number to remember: roughly two and a half hours between the upstream release and a usable WordPress image, on a day when everyone was watching. Your stopgap has to hold at least that long.

the patch

Snapshot first. The Hetzner snapshot of a 9.41 GB disk sat at 0% for a while and took about an hour to become Available, presumably because half of Hetzner was taking snapshots at the same time. I waited. The guard was holding, nothing was on fire, and a rebuild of the PHP container without a way back is not something I do on a day like this.

Then the boring part, which is the good part:

$ docker compose build --pull wordpress
 Image bottega/wordpress:php8.3-fpm-redis Built
$ docker run --rm bottega/wordpress:php8.3-fpm-redis php -v | head -1
PHP 8.3.36 (cli) (built: Oct 21 2026 15:59:09) (NTS)
$ docker compose up -d wordpress
 ✔ Container wp-php Started

Site 200, redis extension loaded, WP-CLI image pulled and also at 8.3.36. Guard removed, nginx restarted, multipart requests back to 200. Second commit, second mail to the list: images work again.

afterwards

The window between the leak and my patch was about twenty-eight hours, most of it with the guard up. Still worth looking. Files changed under the WordPress root since the leak: one directory, the whole tree of a plugin that had updated itself on the evening of the 20th, checksums matching wordpress.org. No PHP files in uploads. No new .user.ini, no new .htaccess. The administrator list unchanged. Both crontabs identical to the copies in git. Clean, as far as I can see, which is the only kind of clean there is.

Two of the test images pruned. 2.2 GB back.

the option I never weighed

Why a guard and not the off switch? Honest answer: because I never put "off" on the table that morning. I had three options in front of me - block multipart, flip the session upload flag, wait for the image - and picked the first. Switching the site off was in the sources I was reading (CERT-EU said isolate exposed applications; a PHP core developer said turn it off for a day if you can afford it) and I skipped past it, because a blog with readers and an editorial team felt like something you don't switch off for a rumour.

Weighed properly, it looks different. Off: full safety, certain cost - ten hours without the blog, without wp-admin, and the length of the outage decided by Docker Hub, not by me. Guard: low cost, the known vector covered, residual risk nobody outside the PHP team could size. On the other side of the scale sat everything an RCE on that container reaches: the database, the environment file with every API key, the uploads, the plugins. A compromise costs weeks. Ten hours cost ten hours.

And "off" never meant stopping containers. One nginx rule returning a static 503 page for everything, nothing passed to PHP-FPM: dark for an attacker, a notice for readers, feeds and cache paused, one line to roll back. Same cost as the guard I did put in, far better coverage. It wasn't on the table because I didn't put it there. Next time it's the first line of the list.

and then...

The thing that stays with me is not the bug. It's how far down the chain the self-hoster sits. PHP fixed it at 14:00. Debian and Ubuntu had packages soon after. The hosting companies patched their fleets. And the person with one box and a docker-compose.yml was still pulling an image built fourteen hours too early, reading GitHub PRs to see whether the build had started. Whatever you put in front of PHP in those hours is the only thing you own.

And the webmail. It had been running a PHP that nobody maintained, on an Alpine that nobody maintained, and I knew neither until I typed php -v with a reason to. Nothing had broken. That's the problem with things that don't break.


Green Lung are still playing. Someone has to be the organist at this kind of funeral, and they are very good at it. Tomorrow the kanban guard stays on until its image catches up, and the webmail stays dark. Tonight the Bottega is patched, the editors can upload their pictures, and I can stop pulling.