Mostly PHP, and mostly unaffected

On 21 October PHP shipped emergency releases for two bugs that have since been named php2shell (CVE-2026-109618 and CVE-2026-109619, both rated 9.2 on CVSS v4). They give remote code execution without authenticating, and they’re in the runtime rather than in anybody’s application code, so every installation that serves web requests is in scope: PHP-FPM, mod_php, php-cgi, FrankenPHP, LiteSpeed. Amazon’s threat intelligence team saw an exploitation attempt on the 17th, four days before any of it was public. The UK’s NCSC was telling people to consider taking systems offline if they can’t patch quickly. No WordPress update and no security plugin helps here, because the thing that needs updating is the PHP that answers the request.

Stelae is a static WordPress host and it’s mostly PHP. Every customer site is a WordPress container (with a phpMyAdmin button next to it), and the export tooling is PHP as well. The next day I pulled a patched image and recreated the containers.

I wasn’t exactly in a rush, and the rest of this post is why. It’s also most of the reason Stelae exists.

What the bug needs

php2shell needs to reach a PHP process that is serving a web request, which is why the advisories single out mod_php as the worst case: exploitation can succeed before any application code runs, so before WordPress has loaded, let alone checked a login. Whatever hardening you’ve done inside the application is downstream of the thing that already went wrong.

So for any PHP site the question is whether an anonymous request from the internet can get as far as the PHP runtime at all, and how well the application behind it is locked down doesn’t come into it.

For a normal WordPress site the answer is yes, by definition, that’s what the site is: Apache or nginx takes the request, PHP renders the page, and that round trip is the product. You can’t gate it behind a login, because the login page is itself PHP rendering a page for an anonymous visitor.

Where the PHP is

Stelae splits that in half. WordPress runs as a private authoring environment in a per-site container, you export the site to static HTML, and the export is what the public gets, on Cloudflare or a git host or statichost.eu. The public site is just files, there’s no PHP behind it to reach, so there’s nothing on the public side for this bug to do.

That leaves the editor, and the editor is real WordPress, running mod_php, the configuration the advisories call the highest risk, so my PHP was vulnerable and the question is who could send it a request.

Editor subdomains go through a Go reverse proxy before they reach the container. Two paths are handled before the auth check, the one that redeems a short-lived token from the editor link and the one that logs you out, and both are answered by Go. Everything else without a valid session cookie gets a page rendered by Go that asks for the editor link, and the proxy never dials the container. A lot of the time there’s no container to dial anyway, because an editor stops after an hour of no access and only starts again when its owner comes back, so the PHP this bug needs isn’t running at all.

There’s no Stelae login in front of the editor at all. Every site has its own secret URL, and that URL is the credential. You bookmark it, visiting it sets a session cookie and forwards you to the subdomain, and nobody you work with needs an account with me to get in. Who they actually are stays WordPress’s problem, they log in there the way they always did. What the secret URL decides is whether a request gets to meet WordPress in the first place, and wp-login.php is behind it rather than in front of it (phpMyAdmin sits behind the same gate, and only runs at all when you start it from the dashboard).

But anybody holding one of those URLs could have exploited this against their own editor, and they wouldn’t have needed a WordPress password to do it, because the secret URL is what gets you to the PHP and the WordPress login sits behind that. A compromised editor could also get its own public site republished with whatever it likes, the same as with a plugin.

It bothers me less than it sounds, because the people with the link are the people the editor belongs to, and a WordPress admin can already execute arbitrary PHP in their own site by design (that’s what the plugin and theme editors are). Each site is its own container on its own network, so it stays there. A link can leak, of course. The owner can regenerate it from the dashboard, which also logs out every session the old link ever handed out.

The export tooling runs PHP as well, but only on the command line (WP-CLI asking WordPress which pages exist) and in an ephemeral container that doesn’t serve web requests, so it’s an affected version with nothing listening.

What I actually had to do

Update the editor images and recreate the containers. Against the part of this that made it an emergency, an anonymous request from the internet, Stelae was effectively immune, because nothing reaches the PHP without the editor link. It was still running an affected version of PHP in the highest-risk configuration there is, on every customer container, so I patched it like everybody else, just without the rush. No public site went offline and nothing woke me up.

How it went elsewhere

By the 24th a researcher had working code execution against mod_php on a honeypot, and a few days later CISA added both bugs to its list of known exploited vulnerabilities.

The clearest real case so far is Palo Alto’s firewalls. Their management interface runs PHP, and their advisory now says exploitation has been observed against management interfaces exposed to the internet. Their recommendation is to let a single jump box reach the interface and nothing else, and their own severity score for the same bug drops from 9.2 to 7.7 when you do. That’s more or less how the Stelae editors have been set up from the start, the Go proxy is the only way in from the internet, and it only lets a request through with the editor link.

Plenty of people took the NCSC’s advice and switched things off. On the 22nd a minister told the House of Lords that 32 UK government services were offline and four more partly down, “largely from precautionary measures”, with no known compromise of central government systems.

Why the editor is still full WordPress

Static HTML was always possible. People have been exporting WordPress to files for a very long time, and none of that is my idea. But the usual way to do it is a plugin, so WordPress keeps running on ordinary PHP hosting, reachable from the internet like any other WordPress site, while the static copy is served from somewhere else. The static copy can’t be attacked the way this bug attacks PHP, it’s just files. Meanwhile you end up paying for full PHP hosting whose only remaining job is an editor that only you use, which has always seemed odd to me, and that editor is still on the internet. If the plugin also does the publishing, which is the convenient setup, the credentials for the static host live in WordPress too, so anyone who can take over that public editor can push whatever they like to the public site.

The other way out is a static site generator, which means giving up the editor, and the editor is the part people actually want: the block editor, the plugin they’ve used for nine years, the media library, the workflow somebody learned once and would rather not learn again.

So Stelae doesn’t make the editor smaller. It keeps WordPress intact and can publish straight to the static site from inside it (the credentials for the static host aren’t accessible from WordPress), but the editor itself isn’t publicly accessible. You keep the ecosystem, and what you give up is dynamic behaviour on the public site, which for a blog or a brochure site or a docs site is usually nothing you were using.

The first post on this blog called Stelae “a publishing layer that keeps WordPress as an editor and puts plain static files on the public internet instead”. The argument in that post was about the cost a capability carries after you ship it, one more thing to understand, maintain, secure and document. Serving your public pages with a language runtime is one of those capabilities, and on a lot of sites nothing actually needs it, but it’s still there for anyone who wants to abuse it in a week like this one.

Too many moving parts

Sam Altman told a group of students at Stanford recently that he no longer thinks we’re going to be able to just patch all the bugs. He was talking about AI finding them faster than anyone can fix them, and I don’t know whether that curve is as steep as he says it is.

The part underneath it I do believe. A normal web stack is an enormous number of pieces that all touch each other, a runtime and a framework and a plugin ecosystem and everything those plugins decided to pull in, and nobody has the whole thing in their head. The interactions are where the bugs live, and the number of interactions goes up every year while the number of people who understand any given stack does not. Patching is what you fall back on once you’ve accepted that, and it only works for as long as you can keep up.

Wikimedia ran into a version of this the same week. It took many of its supporting services offline, Toolforge and Cloud VPS among them, and some of those took longer to come back because “users have direct control over what PHP version they use”, so they stayed down until the volunteers running them had made the changes themselves.

So Stelae is shaped to keep that pile off the internet rather than to patch it faster. The thing answering a request on an editor subdomain is a Go server with four direct dependencies and somewhere between 10,000 and 30,000 lines depending on how you count, most of it billing, backups and the reconciler rather than the part that handles requests, in front of a frontend with no build step and no npm. WordPress, the plugins, PHP, phpMyAdmin and MariaDB all sit behind it, reachable with a session cookie, and on the public side they aren’t in the request path at all. It’s the same pile everybody else runs, just not the part facing outwards.

That doesn’t make it immune to everything. A bug in my proxy or my session handling would land on me exactly the way this one landed on everybody else, but the gate in front of the editors is a few thousand lines I can read end to end, on top of Go’s standard library, whose HTTP server and reverse proxy were built and hardened for exactly this job. Building it this way means fewer moving parts, and fewer of them pointed at strangers, which is the only lever I’ve found that you can pull before a disclosure instead of after one.

The other post I’m publishing today, What one person can actually operate, is the long version of all this: what the system does when nobody is looking at it, and why a service with one operator ends up being built this way. It had been in my pipeline for a while, and now seemed like the perfect time to publish it.

Where it stands

All sites are on the patched build. The public sites were never reachable by this, and the editors weren’t reachable without the link, but they were still running an affected PHP, so they got patched.

If you’ve been putting off the question of how much of your public attack surface needs to be executing code at all, this is a decent week to think about it.

Similar Posts