PHP2Shell is not a bug in any PHP application. It is two bugs in PHP itself, the C program that runs the application, and the advisory for the first of them says the failure happens "entirely before any application-specific code begins execution". For teams shipping an LLM feature, that is the part worth sitting with. Your Python or Node service also runs on a runtime written in C and C++, that runtime also ships memory-safety fixes several times a year, and when it is compromised the attacker holds your provider API keys and can make your "AI app" do things your code never asked for. So when an auditor, a customer or your own incident channel asks "what did the AI app do last week", the honest answer has two halves: which requests actually reached the provider, and which runtime version was serving them at the time. A record that only covers one half cannot answer the question.
What is PHP2Shell?
PHP2Shell is the name given to CVE-2026-109618 and CVE-2026-109619, two critical vulnerabilities in PHP's core, both rated CVSS 9.2. The PHP project published fixes on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, with minimal source backports for the unsupported 7.4, 8.0 and 8.1 branches.
The two advisories describe two different failures in the same layer. In the first, PHP handles an out-of-memory condition with longjmp(), and if the allocation fails while request memory is only partially set up, request shutdown runs over invalid state. In the second, per-request global state can outlive the memory it points to, and a later request on the same worker uses it before normal initialisation resets it. Both are use-after-free bugs, both are reachable by an unauthenticated HTTP request, and both have confirmed code execution against mod_php. The first advisory is explicit that this happens before any application-specific code runs.
The timeline, as Wiz describes it, is also instructive: the first bug was found on 19 August, reported to the PHP security team on 24 August, and patched publicly two months later. Wiz says frontier models contributed to discovery, exploit development and patch validation. Help Net Security reports that Amazon's honeypot network recorded an exploitation attempt on 17 October, four days before disclosure, and that the UK NCSC advised taking any system offline that could not be patched at once.
Why does a PHP bug matter if my AI app is in Python or Node?
Because the layer it lives in is not PHP-specific. CPython is written in C. Node.js sits on V8 and libuv, which are C++ and C. Erlang's BEAM, which runs our own service, is written in C. Every one of these runtimes manages memory by hand on behalf of code that never thinks about it, and every one of them ships security releases on a regular cadence.
This is not a hypothetical. On 29 July 2026 Node.js shipped fixes for eleven vulnerabilities across the 22, 24 and 26 lines, including a heap use-after-free in the HTTP/2 stack, the same class of bug as both PHP2Shell CVEs. Python keeps a similar pace: on 1 October 2026 the project released security updates across five release series at once. None of these is PHP2Shell, and we are not suggesting an equivalent exists in your runtime. The point is narrower: the runtime is a component with its own vulnerability history, it is patched on someone else's schedule, and a bug there bypasses every control you wrote in application code.
What PHP2Shell adds to that picture is pace. Wiz says the bugs affect versions back to PHP 5.4, and its own timeline goes from first discovery to confirmed unauthenticated code execution against Drupal in one day, with frontier models in the loop throughout. It is reasonable to expect more runtime-level findings, found faster, in the runtimes AI applications are built on.
What changes when the runtime is compromised instead of the app?
Most LLM security work assumes the application is intact and the inputs are hostile: prompt injection, PII in prompts, secrets leaking into logs. A runtime compromise inverts that. The process is hostile, and the inputs may look perfectly normal.
Consider what a web worker typically holds in memory or environment at the moment of a PHP2Shell-style compromise. The OpenAI or Anthropic API key. Session data for whichever user is being served. The database credentials the application uses to fetch context for the prompt. An attacker in that position does not need to jailbreak anything. They can call the provider directly with your key, read the responses, invoke whatever tools your agent is allowed to call, and send user data to the model or anywhere else, all from the process the provider sees as you.
Two things break at once. Your application-level logs stop being evidence, because the compromised process writes them. And the provider-side usage dashboard shows that calls were made with your key, but not which user, which feature, or which deployment made them. This is the gap the audit question has to fill. We wrote about the first half of this problem, logging what agents and tools actually did rather than only the chat completion, in AI audit logging in the agent era. PHP2Shell is the argument for the second half.
Why are "what did it do" and "what is it running on" the same audit question?
Every control framework that touches AI systems asks both questions, usually in different sections. SOC 2 asks under CC7 how you detect anomalies and respond to incidents, and elsewhere how you manage changes and vulnerabilities, which we covered in how to make your AI application SOC 2 compliant. The EU AI Act's Article 12 requires high-risk systems to technically allow the automatic recording of events over the system's lifetime, and we covered the revised timeline in EU AI Act high-risk logging: moved to 2027 and 2028.
An auditor working through a runtime CVE does not experience these as separate questions. They ask: when did the vulnerable window open for you, when did it close, and what did the system do in between? To answer, you need to join two records that are usually kept by different teams in different tools.
The first is the record of model activity: one event per request and response, with a timestamp, the environment it came from, the model, the prompt and response content with personal data redacted, any tool calls with their arguments, and whatever identifies the user or session. The second is the record of what was running: runtime version, container image digest, and the time each version went live and was retired.
If these two records share a key, the join is one query. If they do not, the join is a week of reconstructing deploy timestamps from chat history. The cheapest key is to stamp every model request with the build it came from. The proxy we run accepts a JSON string of metadata on each request through the X-SignalVault-Metadata header and stores it with the event, which makes the runtime version and image digest part of the audit record itself:
import OpenAI from "openai";
// Assumes: SIGNALVAULT_API_KEY holds your sk_live_... key and IMAGE_DIGEST is set by your deploy.
const client = new OpenAI({
baseURL: "https://api.signalvault.io/proxy/openai/v1",
apiKey: process.env.OPENAI_API_KEY,
defaultHeaders: {
"X-SignalVault-Key": process.env.SIGNALVAULT_API_KEY,
"X-SignalVault-Metadata": JSON.stringify({
runtime: `node ${process.versions.node}`,
image: process.env.IMAGE_DIGEST,
}),
},
});
Nothing here is specific to Node: the proxy takes plain HTTPS requests, so a PHP app calling OpenAI through Guzzle or curl sets the same base URL and headers.
The same idea works with any logging setup: the point is that the record of a model call should carry the identity of the thing that made it, not just the content. See the proxy docs for the full header list and what each event contains.
One limit matters here. A proxy only records the calls that pass through it, and a compromised process holding your provider key can call the provider directly. The metadata stamp also comes from the process itself, so a compromised process can write whatever it likes into it. What a separate record does give you is a reconciliation: compare the provider's usage, hour by hour, against the requests your record holds for the app that uses that key. Calls the provider billed that your record never saw are calls that bypassed your code, and the hour they start is where the investigation begins.
What should the audit record show after a runtime CVE?
When the next PHP2Shell lands in a runtime you depend on, these are the questions you will be asked, in roughly this order, and the record that answers each.
Which deployments were exposed, and for how long? Answered by the runtime inventory: version per environment, with go-live and patch timestamps.
Which model requests were made from those deployments during the window? Answered by the model-activity record filtered by environment and time, and by the metadata join above.
Did anything about those requests change? Answered by comparing the window against the baseline: new models, new tool names, a jump in request volume, requests arriving at odd hours or from an environment that does not normally talk to the provider. Guardrail violations recorded during the window are part of this picture, since a compromised process may send content your own code would never assemble.
What personal data was in those requests? Answered only if the record exists and the data in it was redacted before storage. This is where redaction and logging stop being separate concerns. A record that stores raw prompts is a second breach waiting inside the first; a record that redacts national identity numbers and card numbers before it writes anything can be handed to the responders without creating new exposure. Our guide to detecting and redacting PII before it reaches an LLM covers where that redaction has to sit.
Which credentials did the process hold? Code execution as the worker account means every environment variable and every file the worker can read. Treat provider keys, database credentials and signing secrets as exposed and rotate them, then confirm in the provider's usage dashboard and in your own record that nothing was sent after rotation.
What to do next
- Inventory the runtime version behind every service that calls OpenAI or Anthropic, and write down who watches that runtime's security announcements.
- Stamp every model request with the build or image that made it, so the activity record and the deployment record share a key.
- Keep one event per request, response and tool call, with environment and user or session metadata, somewhere the application process cannot rewrite.
- Redact personal data and secrets before anything is stored, so the record can be shared with responders and auditors.
- Decide now what "anomalous" means for your model traffic: which models, tools, volumes and environments are normal.
- Write the key-rotation runbook before you need it: provider keys live in the worker's environment and must be assumed read after any runtime compromise.
- When a runtime advisory lands, record the time you learned of it and the time each environment was patched. That interval is the audit window.
Where SignalVault fits
SignalVault is a proxy in front of OpenAI and Anthropic that can redact personal data and secrets before the provider sees them, with rules you set per app, and records every request, response and tool call as an audit event, with the metadata your application attaches. The SignalVault key your application holds can only add to that record: the API it opens sends events and proxies requests, and nothing it can call edits or deletes an event. Events leave the record when your plan's retention period ends, or when someone deletes the app in the dashboard. The proxy refuses to forward a request when a redaction cannot be verified rather than passing it through, and it does not scan or patch your runtime; that job stays with you. If you want the activity half of the audit question answered before the next runtime advisory arrives, you can start a free trial and point your client at the proxy in a few minutes.