Skip to content
DAsikkerhedagent-securityai-agenter

I wrote the warning while the shell was running

In February I told people to audit the projects they'd vibecoded. The shell had been running on my own box for 56 days. It lived 8.5 months — and in October, somebody pointed a model at PHP itself.

On 7 February I posted that everyone should go back and look at the projects they had vibecoded in 2025.

"Hvis du vibecoded projekter i 2025 med React2Shell, er du næsten sikkert eksponeret. Bots have nok allerede grabbed dine environment variabler. Du skal nok roll nogle keys."

That's the original, in the half-Danish, half-English I actually typed it in, slipped verb and all: if you vibecoded projects in 2025 on Next.js versions hit by React2Shell, you're almost certainly exposed. Bots have probably already grabbed your environment variables. You should probably roll some keys.

It wasn't bad advice. It just wasn't pointed in the right direction.

When I wrote it, a web shell had been running on my own server for 56 days. It went in on 13 December 2025. Three weeks before I wrote the warning, the operator had checked in on it again. I found it on 26 August 2026, because Hetzner sent me an abuse report.

I'm writing this now because the week we've just been through makes it relevant again — and because my own case says something precise about what is changing.

The week somebody pointed a model at PHP

The press is calling it Black October.

On 21 October two vulnerabilities landed in PHP: CVE-2026-109618 and CVE-2026-109619, both CVSS 9.2. Unauthenticated remote code execution in request handling itself, before application code gets a say: one through memory corruption when the process runs out of memory, the other through state from one request being reused by the next on the same worker. Every 8.2 through 8.5 release before 8.2.35, 8.3.36, 8.4.27 and 8.5.12 is vulnerable. The Dutch NCSC and CERT-EU both issued warnings.

I patched the same day: both of my Coolify boxes from 4.4.6 to 4.4.7, which brought PHP 8.4.27 with it. A WordPress test instance redeployed. An old URL shortener stopped, because its image hadn't been patched.

What's interesting is how they were found.

It was Calif, a red-teaming firm, that found them — by running OpenAI's GPT-5.6 Sol against PHP under an initiative OpenAI calls Patch the Planet. Wiz independently found both. Coordination then ran for roughly two months before anything went public.

That detail is the whole point. The model wasn't sitting in a lab. It was sitting at a security firm doing its job.

PHP runs on about 70% of all websites. That number is exposure, not damage — Forrester calls it a concentration risk. But it is the size of the surface that had to be patched, because someone pointed a model at the language itself.

What's new isn't that attackers got better. What's new is that an ordinary firm can now set a machine to go looking for vulnerabilities — and that it gets cheaper every month.

26 seconds

My own case is the other end of that story, and it's older.

React2Shell was disclosed on 3 December 2025 — remote code execution through Next.js server components. Ten days later, on 13 December, three apps on my box were compromised.

The third one is the one worth looking at. From the log:

  • 21:51:09 — a POST to the root. It runs for 20,009 milliseconds and aborts.
  • 21:51:35 — GET /_node/v6/patch?0=id returns 200 with 70 bytes.

26 seconds from exploit to working command line. Then a call to /ip-93a1… returning 200 with 14 bytes: a beacon that hands back the box's IP address, so the operator knows what they've got.

And then it went quiet. But not empty.

On 24 December they ran id again. On 16 January again. On 8 March. On 6 July. Four check-ins over half a year, all from Tor exit nodes, all 200. That isn't scanner noise. That's access somebody maintained.

My LinkedIn warning sits between check-in two and check-in three.

The app nobody touches stays owned

The shell was evicted from the other two apps. Not because I did anything.

They were rebuilt on patched Next.js later — one in February, one in May — as a side effect of ordinary deploys. The shell lived inside the container, and a new container is a new container. Neither was a response to anything — I didn't know there was anything to respond to. And a rebuild removes the shell, not the exposure: their secrets had been readable for two and five months respectively.

The third app was never redeployed. It was finished. It worked. There was no reason to touch it.

That's why it lived 8.5 months.

It's the most uncomfortable lesson in the whole thing, because it inverts how you normally think about risk. The app under active development gets patched for free every time someone fixes a button. The app that's finished sits still — and "finished" means, in practice, "pinned to whatever version was current when I lost interest."

I have a handful of those. You probably do too.

The socket was deliberate

Why that particular container was serious, and not just a defaced landing page:

id
uid=1001(nextjs) gid=65533(nogroup) groups=988(docker),65533(nogroup)

The app user was in the docker group, and /var/run/docker.sock was bind-mounted read-write. Nothing needed escalating. docker run -v /:/host was directly available for the entire window.

And the socket wasn't a misconfiguration. The app is a control panel for Docker — it talks to the socket because that is its entire purpose. You can't remove it without removing the app.

So the lesson is sharper than "don't mount docker.sock": a Docker control plane on a public hostname is host root with extra steps. Any RCE there isn't container-scoped. The mitigation is an auth gate in front or a filtering socket proxy — not pretending the socket is optional.

Round two: patched in a day, still blind

Then October came, and I had learned something. I had an inventory. I knew exactly which surfaces ran PHP: Coolify itself on both boxes, a WordPress test instance, a URL shortener. No PHP on the hosts.

And I still couldn't answer the only question that mattered.

Were we hit?

Traefik's access log on one box stopped writing on 18 September. Production Traefik has no access log. Coolify's own nginx doesn't log requests. There was no data to search.

The first time I was owned without knowing. The second time I patched fast and stayed blind. They're the same gap seen from two sides: I had built systems I couldn't ask questions of afterwards.

What didn't happen

Here's the part I think most of these stories skip.

I found nothing on the host. No foreign commands in .bash_history — only my own work. Stock systemd units. Three expected keys in authorized_keys. No trace of an attacker on disk in the December window. The only things provably returned over HTTP were id output and a 14-byte beacon carrying the box's IP address.

But the socket means absence of traces is not evidence of absence. The proxy log can't rule out exfiltration, and a shell with host root for 8.5 months is a shell with host root for 8.5 months. I treated every secret on that box as disclosed, because that's the only defensible assumption.

And the most important boundary: nothing in the logs suggests the attack on me was AI-driven. The break-in itself was a commodity scanner against a known, unpatched vulnerability — 26 seconds from the exploit request to a working shell. The four check-ins afterwards were somebody maintaining access.

The AI part of Black October is on the discovery side, not the exploitation side. Defenders got there first. The attackers who hit me didn't need a model to exploit a bug that had already been found.

Which is why the discovery side is the part to watch.

The worms are still in the lab

There's a lot of talk about AI worms right now, and it's worth being precise about where we actually are.

On 12 October, Anthropic published a study of whether models can replicate without a data center — worms that run the model directly on the hardware they compromise, so the weights become part of the payload. They tested eleven open-weights models. One, GLM-5.2, made it all the way, spreading to 8 of 8 nodes in 4 out of 5 trials. The authors describe their own setup as simplified, with trivial access granted — an upper bound, not an expectation. Their own summary is the sentence worth quoting:

"We don't yet have a full rogue replicator, but agents are becoming increasingly capable."

On 25 September, OpenAI disclosed that it had built self-replicating prompt injections in simulation: an injection that both reaches its goal and gets copied onward to the next agent. Found in June, inside a setup pitting an attacking agent against a defending one. Simulation only.

No outbreaks have been publicly reported. It's research, and it's worth watching — but it isn't what hit me, and it isn't what happened in October.

The third leap

In July I wrote that we were in the middle of a leap where models were becoming interchangeable. Yesterday I went through Anthropic's S-1 and found the numbers behind the same claim: an 85% gross margin, and a cost per task falling faster than the cost per token. A margin that size on something turning into a commodity is exactly the reason to expect the price to keep falling — including from people with no expensive research division to pay for.

The chain looks like this:

  1. The agents started working. In my own experience, somewhere around the end of 2025, they stopped suggesting code and started finishing tasks.
  2. Intelligence became a commodity. More models got good enough for the same job, open-weights models took the volume, and the price started falling.
  3. And now that cheap intelligence is being pointed at every codebase that exists.

The third one is what happened in October. Not that attacks became automated — they've been automated for twenty years. But that the price of finding a new hole fell, on the same curve as everything else.

You can read that as good news, and the reading has something to it. The PHP bugs were found by a security firm, not by an attacker. The coalition got two months. The patch was ready when the world was told what was wrong. Defenders got there first.

But the asymmetry doesn't favor us in the long run, and the reason is mundane: defense has to patch everything. The attacker has to find one thing. When the price of "finding one thing" falls, it falls for both sides — and one side has an inventory to maintain while the other only needs a list of targets.

My own case is that in miniature. I knew what React2Shell was. I wrote the warning. And 18 containers on my box were still running vulnerable Next.js on the day I found out — nearly nine months after the patch existed. The problem was never knowledge. It was keeping track of what I had.

Three things I'm taking with me

Build an inventory, not an intention. The difference between February and October wasn't that I got better at security. It was that in October I could answer "where does PHP run in my estate" in ten minutes. Warnings don't work on your own systems. Lists do.

Log so you can answer afterwards. Patching is the easy half — it can be done in a day. The question "were we hit" cannot be answered later if the data wasn't collected at the time. I've turned access logging on for both boxes — back on in one place, on for the first time in the other, and it's the one change from October that will matter next time.

Rebuild your finished projects — with updated dependencies. That is the precise wording, and the precision is the whole point. The shell disappeared from my other two apps because the rebuild carried Next.js forward with it — their version range let the patch move, so a fresh build picked up a version where the bug was fixed. Had the version been pinned exactly, the same rebuild would have produced exactly the same vulnerable container. A redeploy on its own is not a security action. A redeploy that is allowed to update dependencies is — and it also picks up fixes for vulnerabilities you haven't heard about yet.

And a fourth, technical one I paid for: grep hides matches in log files that contain NUL bytes. The access log was 5.98 GB and contained exactly those, so GNU grep classified it as binary, wrote "binary file matches" to stderr and nothing to stdout. My first search returned zero results, and for a moment I believed the attacker had never got a shell. That was wrong. Use grep -a on access logs.

The warning was right enough in February. I just didn't read it as being about me.