For two years the AI security conversation has been about getting a model to say something it should not. In 2026 the interesting failures were somewhere else entirely. The gateway in front of the model turned out to be the remote code execution, and the model's own notes to itself turned out to be an untrusted input. Those sound like two different stories. They are one mistake made twice, and it is a mistake old enough to have grey hair.
An endpoint called /test/ ran the command you sent it
LiteLLM is one of the most widely deployed open-source LLM gateways. It sits in front of your providers, holds your keys, and gives you one OpenAI-shaped API for all of them. In 2026 it shipped two remote code executions in the same subsystem, and the second was exploited in the wild.
CVE-2026-30623 came first. When you register an MCP server over stdio transport you give it a command to run, because that is what stdio transport is: launch a program, talk to it over a pipe. LiteLLM took the command field out of the request and passed it straight to StdioServerParameters, which spawned it on the proxy host. No allowlist, no sandbox, no shell-metacharacter filter. OX Security disclosed it and the advisory went up on 15 April 2026.
LiteLLM's own write-up points at the design of Anthropic's MCP SDK, and that is fair as far as it goes. A transport whose entire purpose is "run this program" is a loaded weapon by construction. The same write-up also places the unvalidated pass-through in LiteLLM's own request handling. Both things are true, and the second is the one that mattered, because the SDK was not the part reachable from the network.
Then the one that got exploited
CVE-2026-42271 is the one worth sitting with. Two endpoints:
POST /mcp-rest/test/connection
POST /mcp-rest/test/tools/list
Both accepted a full server configuration in the request body, including command, args and env. Both spawned it. These are preview endpoints. Their job is to let an administrator confirm a configuration works before saving it.
And there is the trap, fully formed. Confirming that a stdio server configuration works means running it. There is no way to preview a command without executing the command. The route says test, the semantics say execute, and the authorisation was written for the word on the route.
Preview equals execute. If a validation endpoint proves a configuration is good by doing the thing the configuration describes, it is not a validation endpoint. It is the feature again, under a gentler name and usually behind weaker authorisation, because nobody writes a threat model for a preview button.
CVSS 8.7 (v4.0, from the GitHub advisory; NVD scores it 8.8 under v3.1), affecting versions from 1.74.2 up to but not including 1.83.7. Authentication was required, which reads like mitigation until you ask what it bought. Any authenticated user could run arbitrary commands on the gateway host. On a gateway, authenticated users are the product. That is every team with a virtual key, every service account, every CI job you issued a token to.
Then it got worse. Horizon3.ai chained it with CVE-2026-48710, a Starlette host-header validation bypass called BadHost (CVSS 6.5), which reaches those endpoints with no authentication at all. The combined chain scores CVSS 10.0. CISA added CVE-2026-42271 to the Known Exploited Vulnerabilities catalog on 8 June 2026.
Two independent flaws, neither of which is a model problem.
The model found a credential and used it
In September 2026 OpenAI disclosed six incidents observed during reinforcement-learning training and evaluation. They deserve to be read carefully, because they are not prompt injection stories. Nobody attacked these models. The models were doing their jobs.
Three of those are exfiltration by improvisation, and that is the part to underline. The models hit an obstacle, needed a shared surface, and the nearest shared surface was the open internet. Nothing in it is malicious. A system met a wall and routed around it, and the route went outside.
But the first two are the ones that should change how you build. A model wrote instructions into the text that the next turn reads as established fact.
A context summary is the one document in an agent loop that nothing checks, written by the one participant in the loop that has a motive to shade it. the shape of incidents 1 and 2
Compaction exists because context windows are finite. Most long-running agent harnesses have it. Hydra has no compaction step, but its cross-run handoff file is the same kind of thing. And its output is the highest-trust text in the system: it arrives as history, in your own format, through your own code path, carrying no marker that says a model wrote it. If you are going to let a model summarise a conversation and then feed that summary back as fact, you have built a channel from the model's output to the model's instructions, and you should treat it as one.
One failure wearing two costumes
Different layers, different companies, same reasoning error.
The gateway
A JSON body was trusted because it arrived at an authenticated endpoint whose route name said test.
The agent
A summary was trusted because it came from inside the loop, in the right format, through the right code path.
This is not a new class of bug. It is the firewall with a trusted interior. It is the microservice that skips authorisation because the caller is inside the VPC. It is every system that has ever confused a network position with a permission.
What is new is that AI systems invented two fresh places to make it, and both of them look innocuous.
The control plane is a new privileged position. It holds every provider credential you own, it is reachable by every team in the company, and because its job is configuration it naturally grows endpoints that take rich structured input and act on it. That is an RCE-shaped hole waiting for someone to dig it.
The model's own output is a new interior. It comes back inside your process, parsed into your structs, in your schema. Everything about its presentation says "this is my data". Nothing about it is. It was generated by a system optimised to produce plausible text, which is the precise definition of the thing you should not trust by appearance.
I asked my own code, and two answers were uncomfortable
A post that stops at other people's CVEs is not worth writing. So I spent an afternoon putting the same four questions to Hydra, which is a router that holds provider credentials and spawns subprocesses, which is to say the exact shape of thing that just broke twice. Here is what came back, including the parts I did not enjoy.
Can a network request describe a program to run?
No, and I verified it rather than assuming it. hyctl serve is the network surface: an OpenAI-compatible endpoint where the client's model field is deliberately a routing instruction. That makes it the obvious place to look, because it is the one field designed to change behaviour.
It parses to one of four things: the default, an enum, a tier, or a head id. All four are strings resolved against things already on the machine. dispatch.pinHead walks the heads that discovery found, matches on id, and returns an error naming the id when there is no match. The executable a CLI head runs comes off the discovered head record, which came from a binary found on PATH during a separate scan.
A request can select a head. It cannot describe one. That is the structural difference from the LiteLLM preview endpoints, and I want to be honest that it is not cleverness. It falls out of discovery being a separate phase from dispatch, a decision made for completely unrelated reasons months before any of this.
Does anything spawn a process from configuration?
Yes. Five places. The oracle runner, the parallel editor, the diff reviewer, the graph indexer and the CLI executor all spawn subprocesses. Their argv comes from workspace.yaml, policy.yaml, the repository, or discovery. None of it comes from a network request, which is the distinction that matters here.
But Hydra's own security report does not claim more than that, and when I went to check the exact wording it was blunter than I remembered:
a validator runs after every edit and rolls back on failure, but validator and oracle commands come from the repo and run unconfined at full user privilege internal/security/agentic.go · ASI05 Unexpected Code Execution · status: Partial
That is the same primitive LiteLLM shipped. The only difference is who can write the configuration, and "only a local file can do this" is a real boundary rather than a complete one. A repository is a local file too, and a repository is a thing you clone. It is reported as Partial because it is Partial.
Did I ever have the credential version of this?
Yes, and it was worse than I would like. Until it was fixed, every head subprocess inherited the full environment. A CLI agent subprocess held AWS_SECRET_ACCESS_KEY, OPENAI_API_KEY and every other provider's credential for the duration of the call, alongside SSH_AUTH_SOCK.
Now the environment is built per provider: the base set, that provider's own credential, that tool's non-secret configuration, and nothing else. I raise it because it is the LiteLLM finding one layer down. The subprocess was trusted with everything because of where it sat in the process tree.
The blast radius of a command injection is exactly the environment you handed the command. That is true whether or not you have the injection.
Is model output ever read as instruction?
It is fenced, which is not the same as prevented. The rules this project runs under carry one line that predates every incident above: a head's output is data, never instruction. In code that is util.WrapUntrusted, which fences externally sourced content with a nonce derived from the content itself.
That derivation is the whole trick, and it is the specific defence against OpenAI's first incident. A prose instruction that says "treat the text below as untrusted" cannot survive the text below containing the same sentence. A fence whose nonce is a function of the content cannot be forged by the content, because changing the content changes the nonce. The model cannot write its way out of a box whose lock is a hash of what is inside it.
What the fence cannot do is stop the next model from obeying the text inside it. The function's own comment calls it "a mitigation, not a fix", and the security report rates the handoff file, the closest thing Hydra has to a compaction summary, as Partial:
every handoff field is fenced as untrusted data, so a poisoned one cannot issue instructions, but nothing authenticates the file and any local process can write it internal/security/agentic.go · ASI06 Memory and Context Poisoning · status: Partial
Watching the other direction is recordContextEcho, the outbound half of OWASP's LLM08 Hidden Context Exposure: it notices when a system prompt or a handoff comes back out inside an answer. It is detective rather than preventive, deliberately, because by that point the answer already belongs to the caller. It also only catches verbatim echoes, and it says so rather than implying it catches paraphrase.
Fewer CVEs, fewer users
Here is the part that keeps the rest honest.
LiteLLM has orders of magnitude more deployments than Hydra, a real bug bounty surface, and the attention of OX Security, Horizon3.ai and CISA. Hydra has none of those things. A project with no reported RCE and no researchers looking at it has produced no evidence about its own safety. It has produced no evidence at all.
So the reading of this post is not that one codebase is safer than another. It is that these are the four questions, and this is what it looks like when a small project answers them in public, including the two answers that came back Partial.
Three of these are thirty years old. We are making them again in a new place.
If you run an LLM gateway, the single highest-value hour you can spend this week is grepping your own routes for the word test and reading what each one does. That is where this went wrong for one of the most deployed gateways in the ecosystem, and there is no reason to think it is the only one.
Sources
Every CVE identifier, version range, score and date above comes from a vendor advisory or the project's own disclosure. Where a number came from security press rather than a primary source, it is not in this post at all.