The Attack Came In Before My Code Ran — Why I Dug Into PHP2Shell Without Using PHP#
2026-11-01

The line I skimmed past in the brief#
The lead item in last week’s AI news brief for October 24, 2026 was PHP2Shell. Two vulnerabilities in PHP allowed remote code execution without authentication, and security researchers using AI tools were the ones who found them. In the brief, I framed the story as “finding got faster, but the capacity to fix didn’t keep up.”
After I finished the brief, one phrase kept nagging at me. I had written “AI-found vulnerabilities,” yet I couldn’t explain how those vulnerabilities actually hand a server over to an attacker. I don’t use PHP, so “upgrade to the patched version” isn’t advice that applies to me. But without understanding the mechanism, I had no way to tell whether the same kind of hole exists in the systems I run.
So I read the PHP Foundation’s announcement and the security vendors’ write-ups and traced the mechanism. Here is the conclusion up front. This attack did not come in through a bug in application code. It came in through the layers that run before the application code, and through the cleanup that runs after an error. This post explains that mechanism with a simple example, then uses what I took from it to review one of my own services.
I leave out anything detailed enough to reproduce the attack. I only go as far as the mechanism a defender needs to make decisions.
What happened#
On October 21, PHP released 8.5.12, 8.4.27, 8.3.36, and 8.2.35, fixing two vulnerabilities (CVE-2026-109618 and CVE-2026-109619). Both scored 9.2 on CVSS (Common Vulnerability Scoring System, a severity score for vulnerabilities), which puts them in the “critical” tier. In its announcement, the PHP Foundation wrote that on Apache with PHP loaded as a module, an unauthenticated request can trigger the problem before any application code runs. Wiz found that nearly every PHP version from the last decade, going back as far as 5.4, is affected by at least one of them.
The problem had a history. A 2024 external security audit had already raised the possibility that out-of-memory conditions could be exploited. The conclusion at the time, though, was that an exploit looked theoretically possible but no concrete attack path had been found, and other issues with clearer impact took priority. In the summer of 2026, security researchers using AI tools turned that theory into a working attack. Calif started its audit on July 30 and reported unauthenticated remote code execution on August 11, and Wiz and OpenAI followed with reports of other attack paths. In its write-up, Wiz said it used AI models throughout the research. According to the timeline it published, it found the bug on August 19 and confirmed unauthenticated remote code execution on Drupal the next day.
The people fixing it couldn’t keep that pace. The foundation acknowledged the delay as “a structural failure, caused by both the security team’s decision process and its lack of resources.” I come back to this speed gap later in the post. First, the mechanism.
The mechanism, traced through a message board#
Four terms to know first#
- RCE (Remote Code Execution): An attacker runs commands of their choosing on someone else’s server just by sending requests over the network. “Unauthenticated” in front means not even a login is needed. It is effectively handing over the server, which is why it sits in the most severe tier.
- Memory limit: PHP caps how much memory a single request can use. When a request goes over the cap, PHP abandons whatever it was doing on the spot and ends the request.
- Use-after-free: A bug where a program touches memory it has already given back as if it were still in use. If other data has moved into that spot, the program trusts the wrong values and acts on them.
- ASLR (Address Space Layout Randomization): An operating system defense that shuffles where code and data sit in memory every time a program starts. Even if an attacker corrupts memory, not knowing where to point usually just crashes the server. That is why a real attack needs one more step: an information leak that reveals the memory layout.
What happens on “Neighborhood Market”#
Imagine a neighborhood secondhand marketplace board built with PHP. Members can attach photos when they write a post.
Normally it works like this. The browser bundles the title, the body, and the photo into a single request. But before the board’s code ever receives that request, the PHP engine unpacks it and sorts it into fields. A feature that records upload progress runs alongside this step. It is on by default, and according to Wiz and Datadog it runs even if the board never uses sessions. Only after this sorting finishes does the board’s code run, and only then does it check whether the user is logged in and what kind of file was attached.
The attacker doesn’t even sign up. They send a request straight to the post-writing address. It isn’t about uploading a big file. They shape the request so that PHP uses a huge amount of memory internally while sorting it. Putting the vendors’ analyses together, the attack breaks down into roughly four steps.
- Mapping the memory: A vulnerable server could send fragments of its own memory back inside a response cookie value. The attacker uses this to learn the memory layout that ASLR had shuffled.
- Pushing to the limit: The attacker makes PHP hit its memory limit at a moment of their choosing while it is still sorting the request.
- A half-written slot: According to Datadog’s analysis, when PHP adds an input field to an internal table, it first bumps the count to say “one more entry,” and only then allocates memory for the contents. If the limit hits in between, a slot is left that is counted but empty. That slot holds leftovers from whatever data last used the same memory, and the attacker can fill those leftovers in advance with data they sent.
- Cleanup misreads: Having given up on the request, PHP cleans up. It walks through as many slots as the count says, and in doing so it trusts the leftovers as valid data. In the end, execution follows whatever the leftovers say.
It doesn’t plant code; it calls what is already there#
The part that confused me longest was “how does the attacker get their code onto the server?” The answer was: they don’t.
The server already contains a feature that runs operating system commands. And the request body the attacker sends is loaded into server memory as-is so it can be processed. Up to here, both are normal. Once memory is corrupted, the cleanup routine reads the attacker’s data as an internal instruction: “call this feature with this value.” The memory map from step 1 is what points to where that feature lives.
As an analogy, it isn’t walking in with a weapon. It is closer to cleverly damaging one box on an intake form so the clerk reads that box as an approval to “open the vault.” ASLR is the mechanism that changes which floor and room the vault is in every time, and the leak in step 1 is quietly finding out the room number.
Once a single command runs, the attacker usually writes a small PHP file to the server. This is a back door called a web shell. From then on, they can call that file to come and go even without the bug. The scene people usually picture as “planting code” only shows up at the very end of the attack.
What’s left for the board’s operator#
In this attack, not a single line of the board’s code ran. The login check, the file-type check, and any homemade security filter were all bypassed entirely. According to Datadog, the server log may show nothing but a single error saying the memory limit was exceeded before any script even started (Unknown on line 0).
An attacker who can run commands can read the database password from the board’s configuration file and take the entire member database. They can inject malicious scripts into posts to go after visitors, or move on to other systems on the same server or the internal network. No matter how well the board’s developer wrote their code, it couldn’t have stopped this attack. So the PHP Foundation and the security vendors reached a single conclusion: updating to a PHP version with the vulnerabilities fixed is the only complete remedy.
PHP’s fix wasn’t a patch to a single buggy function, either. Even when the memory limit is exceeded, the error is now deferred until in-progress work reaches a safe point, and if memory keeps growing, the worker process is terminated immediately without running cleanup. Internal state left over between requests is also cleared before reuse. Datadog warned, however, that the pattern of “updating the count before allocating the contents” is still there, so similar bugs could show up again.
What carries over outside PHP#
I use Node.js a lot when I build services. JavaScript doesn’t manage memory directly, so the exact memory corruption above won’t happen in code I write. That still didn’t let me relax. Node.js’s HTTP parser, the core of its runtime, and a good number of native packages are written in C or C++. And even in languages that don’t manage memory directly, the same shape of mistake repeats at the logic level.
I distilled four language-independent checks from this incident. What follows is my interpretation, not an official analysis.
1. There are layers that run before my code#
A request to a web service passes through several layers before it reaches the developer’s code. There is a web server or proxy in front, and then the runtime or framework parses the request body, receives uploaded files, and reads the session cookie. In Express, body-parsing middleware does this job; in Django or FastAPI, the framework’s request parser does. These layers run before the auth check. In other words, they accept and process requests from anyone.
When developers review security, they usually look at the right side: their own input validation and permission checks. PHP2Shell blew up on the left. So the first check is this: Before my code sees a request from someone who isn’t logged in, what processes it, and how much? Does that processing have caps on size and count?
2. A failure leaves half-finished state behind#
PHP’s bug started with the order “bump the count first, fill in the contents later.” That order is perfectly fine most of the time. It only leaves false state behind when something fails in the middle.
Languages that don’t manage memory directly have the same shape. Code that marks an order as “paid” before calling an external payment API, code that adds a file to an attachment list before the upload actually happens, and work that is only partly applied during a retry all fit the pattern. If a request is cut off midway by a timeout or by running out of memory, that state remains, and the next step trusts it. The second check is this: Does the next step avoid blindly trusting the state a failed request left behind? Error paths are also where tests are thinnest.
3. State survives between requests#
The second CVE was a case where internal state used by a previous request lingered even after its memory was given back, and the next request on the same worker process touched it. This happens because a PHP worker handles several requests one after another.
Node.js has this trait even more strongly. One process handles every user’s requests at the same time, so module-level variables and in-memory caches are shared by all requests. Incidents where one user’s information ends up in the next user’s response come from exactly this structure. The third check is this: Is a value that should belong to a single request kept from lingering at the process level?
4. A small leak becomes a stepping stone to a big attack#
In this attack, the leak of memory fragments into a cookie doesn’t hand over the server by itself. But without it, ASLR makes the attack fail most of the time. A small leak made the big attack possible.
Common leaks in web services are stack traces sent straight out in error responses, error messages that reveal internal paths and library versions, and status pages left open for debugging. Each one looks trivial, but to an attacker it is a map. The fourth check is this: When an error happens, is a generic message the only thing that goes out?
Applying it to my own service#
With these four questions in hand, I reopened a small Node.js web service that I run. I reviewed it by reading the repository’s code and operational docs; I did not check the production server’s configuration directly this time.
More was in good shape than I expected.
- Caps on request parsing: Request body parsing had per-route caps on size and on the number of fields.
- Data stays data: The design docs state that code users submit is stored only as data and never executed on the server. Actual execution happens only inside the user’s browser. In effect, the design had already protected the “boundary between data and instructions” that PHP2Shell broke.
- Failure handling: Operations that change several tables, such as account deletion, are wrapped in a single transaction.
- Error responses: When an error happens, only a generic message goes back, and the details stay in the server log.
- Dependency checks: The deployment pipeline includes a dependency vulnerability check.
There were gaps, too.
- Caps on real-time connections: HTTP requests had explicit size caps, but the cap on messages over real-time connections was left to the library’s default. A default exists, so it isn’t an immediate danger, but leaving the caps of a layer that runs before my code to “the default will take care of it” isn’t an answer to the first check.
- Caps at the front web server: The front web server’s request size cap lives in production configuration outside the repository, so I couldn’t confirm it from code alone.
- Blast radius if breached: A small service usually has the app and its data on one server. If the app process is compromised, member data and secret keys can be read with the same permissions. And there was nothing in place to notice abnormal behavior such as the web process launching a shell.
The biggest catch was the runtime itself. This service’s Node.js was stuck on a major version whose official support had already ended. The dependency check in the deployment pipeline reported “0 vulnerabilities” every time, but that check only looks at the libraries the project pulls in. The runtime those libraries sit on wasn’t in scope. PHP2Shell blew up in exactly that layer, the runtime beneath the application. According to Zscaler’s summary, this patch also shipped only for supported PHP 8.2 and later, and end-of-life versions 8.1 and earlier have no official patch. If the same thing happened in Node.js, my service would be on the side with no patch to install at all.
So I planned the following improvements.
- Upgrade the runtime to a supported LTS (Long-Term Support) version. And check the runtime’s end-of-support date in the same place as the dependency check.
- Make every cap explicit in the layers that run before my code. Write the real-time connection message cap into the code, and confirm the front web server’s request size cap in production configuration and record it in the operational docs.
- Lock in a test that values don’t mix between requests. The test checks that when several users send requests at once, each gets back only their own values.
- Prepare for a breach. Look into limiting the web process’s outbound connections to only what it truly needs, and into monitoring that alerts when the web process launches a shell or a download tool. Datadog also named this signal the most direct evidence of a successful attack. This kind of monitoring catches the same class of attack regardless of language.
Speed of fixing is the last line of defense#
Once I understood the mechanism, the “speed gap” I’d written about in the brief read differently. In the end, none of my checks can stop a bug inside the runtime. Against a flaw like PHP2Shell, an application operator has one weapon: how quickly they install the patch once it’s out.
That window is shorter than you’d think. CVE details and a demo video leaked the day before disclosure, and on the morning of disclosure, one distributor shipped fixed images early. VulnCheck observed related scanning starting right after that, at 08:37 UTC on October 21, more than five hours before the official release. Zscaler reported observing more than a thousand exploitation attempts in the 48 hours after disclosure. The finders confirmed a working attack within a day using AI tools, and the attackers moved within hours of disclosure. Yet the fixers took about ten weeks from receiving the real attack report to shipping a patch.
This runs into another piece of advice from the same brief. Covering the LiteLLM supply chain incident, I recommended a cooldown policy of waiting a few days before adopting new versions, because even a release signed by an official pipeline can be malicious. But waiting a few days on a security patch is too late. The answer I settled on is to separate two kinds of updates. Feature updates and ordinary dependencies get a cooldown. Security releases for the runtime and core components get installed right away, after confirming the official announcement and the distribution source.
That only works if a “ready to update immediately” state is in place ahead of time. An end-of-life runtime is not in that state. There’s no patch coming to install.
Wrapping up#
The reason I, someone who doesn’t use PHP, dug into a PHP vulnerability is that without the mechanism, I couldn’t choose where to look. Once I traced it, the places to look turned out to be outside where I usually look: the layers that process requests before my code runs, the cleanup after a failure, state that lingers between requests, leaks that seem trivial, and the runtime underneath all of it.
What this incident showed is less that AI is good at finding vulnerabilities and more that a judgment like “theoretically risky, but no real attack path” now has a short shelf life. A theory set aside in 2024 became a real attack within weeks in 2026, in the hands of researchers using AI tools. My service also had items I had put off with “surely it won’t come to that.” I’ll start with the runtime upgrade, apply the rest of the improvements, and then write up the results.
References#
- PHP Foundation — Today’s PHP security release and how we’re strengthening PHP security
- Wiz — PHP2Shell analysis
- Datadog Security Labs — PHP2Shell detection, analysis, and remediation
- VulnCheck — PHP2Shell vulnerabilities and observed exploitation
- Zscaler — Analysis of CVE-2026-109618 and CVE-2026-109619
- AI News Brief — October 24, 2026