AI Just Found a 14-Year-Old Hole in PHP. What Will It Find in Your Code?
7 min read
Contents
- What actually broke
- The part that should make you uncomfortable
- My take
- 1. The failure path is the real spec
- 2. Know who owns every piece of state
- 3. Limits belong at the edge, and in code
- 4. Design for blast radius
- 5. Debugging means finding the root cause, not the symptom
- 6. Know what you run, and who patches it
- So what is the engineer for?
- What I check in every agent-written change
On the morning of October 20, someone posted on Hacker News that two critical PHP vulnerabilities were about to go public. The post came down fast. The screenshots did not. By the evening the PHP team had pulled its own release forward by a day, and on October 21 it shipped fixes for both: CVE-2026-109618 and CVE-2026-109619, rated 9.2 out of 10, nicknamed php2shell.
Unauthenticated remote code execution. In the language that, by W3Techs’ count, runs about 69% of websites whose server language is known. Amazon’s honeypots had seen an attempt on October 17, before anything was public.
I don’t write PHP for a living. I still think this is the most useful security story of the year for anyone who writes software with an AI agent next to them, which today is nearly everyone. Here is why.
What actually broke
Neither bug is in someone’s plugin or a forgotten admin page. Both are in PHP itself, and both live in the parts of a program nobody demos.
The first one is about running out of memory. When PHP hits its memory limit in the middle of handling a request, it stops. If it stops while some internal request data is only half set up, the cleanup code that runs afterwards treats that half-built data as valid. Parsing an upload into $_POST and $_FILES is one place this can happen. According to PHP’s advisory, an exploit exists where an unauthenticated request triggers it before any application code runs.
The second one is about state that outlives its request. PHP workers are reused. One process handles your request, then mine. Some request-owned state could survive after its memory was freed, and the next request on the same worker could touch it before it was reset. A use-after-free, reachable through session upload progress and, on PHP 8.5, fatal error backtraces.
Read those two again and strip the PHP out:
- What happens when the code fails halfway through?
- What is left behind when one unit of work ends and the next one begins?
Those are not PHP questions. Every backend, every app, every agent loop has them.
The part that should make you uncomfortable
Calif and Wiz, two of the teams credited by the PHP Foundation, have said they used AI models in the work that found these exploit paths, and Calif did it through OpenAI’s Patch the Planet initiative. Calif traced the earliest affected code path back to PHP 5.4, released in 2012.
So: code written by experienced C developers, reviewed for over a decade, used by millions of servers, and a bug class sat there until machines started reading error paths at a scale no human team will.
That cuts both ways. Defenders now have that power. So does everyone else. The window between “a bug exists” and “someone found it” is getting very short.
Now picture code that nobody reviewed at all.
My take
I use coding agents every day. I wrote a whole post about handing a spec to an agent and reviewing what comes back. I am not here to tell you to stop.
But vibe coding, in the sense of describing what you want, accepting what comes back and moving on when it seems to work, has a blind spot, and php2shell sits right inside it. An agent writes the path you asked for. Attackers live on the paths you didn’t.
You asked for “upload a profile photo”. You didn’t ask “what happens if the upload is 2 GB, or has 40,000 form fields, or the process is killed halfway through writing it”. The code works in the demo. The demo is the only path that was ever run.
Here is what I think an engineer still owns, agent or no agent.
1. The failure path is the real spec
For every operation, the interesting questions start after the happy path. What if it times out? What if memory runs out? What if it fails after step two of four? What state is left behind, and who cleans it up?
An agent will happily write the cleanup code. It will rarely ask whether cleanup can run on half-built data, because you didn’t. php2shell’s first bug is exactly that.
2. Know who owns every piece of state
Long-lived workers, connection pools, singletons, caches, a global in a Flutter app, a conversation history in an LLM loop. Anything that survives one request and is visible to the next is a place for bugs to hide.
I hit a small version of this building an AI drive-thru agent. The chat history is pruned to the last 20 messages. Prune in the wrong place and you cut a tool call away from its result, and the API rejects every request after that. The fix was not a better prompt. It was deciding exactly what the history owns and what it doesn’t, then writing that down in code.
3. Limits belong at the edge, and in code
PHP’s interim advice is telling. Enforce request size, field counts, multipart part counts, header counts and nesting depth at the web server or proxy, before PHP ever sees the request. Set the mitigations in server config, because the advisory says ini_set() and .user.ini cannot be relied on.
Where a setting lives is an architecture decision. So is where a limit is enforced. In the drive-thru agent, the 50-item cap per menu item is a few lines in the tool executor, not a sentence in the prompt. The model can’t argue with it. An attacker can’t upload around a limit the proxy already enforced.
4. Design for blast radius
One of PHP’s suggestions is a container or cgroup limit that kills only the affected worker. That is the whole idea of defense in depth in one sentence. Assume something will break. Decide in advance how much breaks with it.
Run the worker as a user that can’t write to your code. Keep secrets out of the process that parses untrusted input. Keep keys out of the app binary, which is one of two rules I keep in every app. None of this stops a bug. All of it decides whether a bug is an incident or a disaster.
5. Debugging means finding the root cause, not the symptom
When a bug shows up, the agent’s instinct, and honestly ours too, is to make the error go away. Add a null check. Catch the exception. Retry.
php2shell’s fix is not “catch the out-of-memory error”. It is “never let cleanup trust data that was only half initialized”. You only get there by reproducing the failure, reading what the state actually was at that moment and asking why it was allowed to be that way. That is debugging. Patching the symptom is how a bug survives fourteen years.
6. Know what you run, and who patches it
The first thing every advisory said was not technical: make a list. Every server, container, CI image and dev machine running PHP, and its version. Then: who applies the update? You, or your host? PHP 8.1 and older get no official release, only source backports. Branches older than 7.4 get nothing.
An agent can scaffold a Dockerfile in seconds. It does not know that the base image it picked is end-of-life, and it will not be around in two years when the CVE lands.
So what is the engineer for?
Not typing. The agent types faster than I do and I am glad of it.
The engineer is the one who asks the questions nobody put in the prompt. What happens when this fails? Who owns this state? Where is the limit enforced? How far does the damage spread? Who patches it, and when? These questions are boring, they don’t make a good demo, and they are the whole difference between software that works and software that is safe.
php2shell was found in code written by experts. With AI now reading error paths at that scale, the bugs in unreviewed code will be found too, by whoever looks first.
Use the agent. Then be the engineer.
What I check in every agent-written change
A short list I actually use. None of it needs a security team.
- Inputs: where are size, count and depth limits enforced, and is it before parsing?
- Failure: what happens if this fails halfway? Is any half-built state reachable afterwards?
- State: does anything here outlive the request, session or loop? Who resets it?
- Secrets: does this process handle untrusted input and hold a key at the same time?
- Config: is the security setting in server config, or in code the app can override?
- Dependencies: is every runtime and base image on a supported branch? Who updates it?
- Root cause: for every bug fix, can I say why it happened, not just why it stopped?
Sources: PHP security advisories, Tenable FAQ, Huntress, SiteGround, Help Net Security.