The Server That Wasn't There: PHP2Shell and an AI Security Audit
Published on
A Bad Week to Be a Web Server
If you run anything on the internet, this week has not been relaxing.
On 21 October, the PHP project shipped emergency releases to fix two critical vulnerabilities, CVE-2026-109618 and CVE-2026-109619, now collectively known as "PHP2Shell". The short version: an attacker who can send a web request to a PHP server can, in the worst case, run their own code on it. No login required. No user needs to click anything. Just a request.
It got worse from there. Details leaked before the planned disclosure, which forced the advisory out early. Amazon's threat intelligence team reported exploitation attempts starting on 17 October - four days before the fix existed. And PHP's own guidance was blunt: there is no complete configuration workaround. The only real fix is to upgrade to 8.5.12, 8.4.27, 8.3.36 or 8.2.35. If you are on PHP 8.1 or older, the branches that no longer get regular releases, you are in for an uncomfortable week.
PHP quietly powers a huge slice of the web - WordPress, most shared hosting, countless control panels and forgotten admin pages. So my first reaction was the same as everyone else's: am I exposed?
The Audit
Rather than poking around myself, I handed the question to an AI coding agent with access to this site's repository. The instruction was deliberately vague: check that this site is up to date and secure with no known vulnerabilities.
On the PHP question, it took about two seconds to reach a verdict. The repository contains zero PHP files. As I described in Giving the Starfish a Proper Home, this site is built with Eleventy, which turns Markdown into plain HTML files ahead of time. Cloudflare then hands those files to your browser exactly as they are. There is no PHP interpreter, no database and no application server sitting behind the starfish waiting to be talked into doing something it shouldn't.
PHP2Shell needs a running PHP process to attack. On this site, there isn't one. The server wasn't there.
That would have made for a very short blog post. Fortunately (or unfortunately), the agent kept going.
Ten High-Severity Surprises
"No PHP" does not mean "no code". A static site still has a build toolchain: Eleventy, Tailwind CSS, an image optimiser and the hundreds of small packages they quietly pull in. The agent ran npm audit, the standard health check for JavaScript projects, and got this back:
10 high-severity vulnerabilities.
That number sounds alarming, so it's worth unpacking what it actually meant:
- markdown-it, the library that turns my Markdown into HTML, had two denial-of-service bugs where carefully crafted text could make it take a very long time to render. Fixed by a simple patch release.
- braces, a tiny library for expanding file patterns like
src/**/*.{md,njk}, had a stack-exhaustion bug triggered by deeply nested patterns. It was being pulled in by both Eleventy and Tailwind.
The agent updated Eleventy, which had already swapped out the offending file watcher in its latest release, and the count dropped to six. Then it hit a wall.
The Fix That Doesn't Exist
The remaining six warnings were all the same braces bug, now arriving only via Tailwind CSS v3. The obvious move would be to upgrade braces to a fixed version.
There isn't one. The last release of braces was published in May 2024. The advisory lists every version as vulnerable, and nobody has shipped a patch. You can't update your way out of a problem when there's nothing to update to.
This is where I appreciated the agent being straightforward about risk rather than just chasing a green tick. The bug only triggers if an attacker can feed malicious file patterns into my build - patterns that I write myself in config files. None of this code ever reaches a visitor's browser. Realistically, the risk was close to zero.
But I'd asked for no known vulnerabilities, so I told it to upgrade everything anyway. The only way to get rid of braces was to remove the thing that depended on it: a migration from Tailwind CSS v3 to v4.
Whack-a-Mole
Tailwind v4 is a substantial rewrite. The agent ran Tailwind's official upgrade tool, which converted the stylesheet to the new format and renamed a couple of classes (flex-grow became the tidier grow, and the old rounded became rounded-sm because v4 shifted the whole scale down a notch). My custom amber colour scheme for the blog text carried over untouched.
Then it ran the audit again. Braces was back.
Tailwind v4 moved its command-line tool into a separate package, and that package uses a file watcher which, three layers down, depends on... braces. Same bug, different front door.
The fix turned out to be pleasingly small. A newer release of the file watcher had already dropped braces in favour of a different pattern-matching library, but Tailwind had pinned the older version exactly. A one-line override in package.json told npm to use the newer watcher instead. Result: 0 vulnerabilities, with nothing left out of date.
Trust, but Screenshot
Here's the part I found most interesting. A major CSS framework upgrade is precisely the kind of change that breaks a site in subtle ways: a margin that shifts, a colour that goes slightly off, a button that loses its corners. Normally you'd click through every page and squint.
Instead, before touching anything, the agent took full-page screenshots of all 13 pages at both desktop and mobile widths - 26 images in total. After the migration, it took them again and compared the two sets pixel by pixel.
The verdict: identical, apart from sub-pixel differences in text smoothing and a one-pixel vertical nudge on a few mobile pages. It then looked at the biggest differences side by side to confirm they were invisible to a human, rather than just reporting a number.
There was also a nice moment of self-correction. When it tested the live-reload development mode, the stylesheet didn't update. Rather than declaring the upgrade broken, it dug in and found the real cause: Tailwind's watcher deliberately stops when its input stream closes, which is exactly what happens in the agent's own background test environment. Re-tested the way I'd actually use it, it worked fine. The bug was in the test, not the site.
The whole thing then went through the normal pipeline: a pull request, an automated build, a Cloudflare preview deployment, and a merge once everything was green.
What I Actually Learned
1. The most secure code is the code you don't run. PHP2Shell is a nightmare for anyone with a PHP server. This site was never exposed, not because of anything clever I did, but because there's nothing on the server to attack. Every database, plugin and admin panel you run is something you have to patch at 2am when the next PHP2Shell drops.
2. Static doesn't mean invulnerable. The risk moves rather than disappearing. It lives in the build toolchain, the hosting account and the domain settings. Those still need looking after - they're just a far smaller and calmer target.
3. "High severity" needs context. Ten high-severity warnings sounds like a crisis. In reality, they were build-time denial-of-service bugs that no visitor could ever trigger. Still worth fixing, but not worth panicking over - and that judgement call is exactly where a good assistant should explain rather than just obey.
4. AI agents are at their best when they check their own work. The impressive part wasn't the upgrades; any competent developer could do those. It was the before-and-after screenshots, the refusal to accept a passing build as proof, and catching its own testing mistake. As I argued in Behind the Magic Trick, there's no sorcery here - just a well-built loop of tools, checks and feedback.
The Bottom Line
If you run PHP anywhere - a WordPress blog, a client site, that admin page you set up years ago and forgot about - stop reading and go and patch it.
If you don't, take a moment to enjoy the quiet. And maybe run npm audit anyway. You might be surprised what's hiding in your toolchain.
As for the starfish, he remains blissfully unaware that any of this happened. Which, honestly, is exactly how security should feel.