Skip to main content
April 22, 2026Alex Frazer

The MCP STDIO RCE class, and why Runlayer doesn't run what the LLM asks it to

OX Security found a design-level flaw in Anthropic's Model Context Protocol. MCP's STDIO transport turns a config file into a command executor. Here's how Runlayer's control plane breaks each of the four attack vectors.

The MCP STDIO RCE class, and why Runlayer doesn't run what the LLM asks it to

OX Security published research this week on a design-level flaw in Anthropic’s Model Context Protocol. The short version: MCP’s STDIO transport turns a configuration file into a command executor. Anyone who can write to that config, whether through prompt injection, a poisoned marketplace entry, or a malicious repo’s mcp.json, can run arbitrary code on the host with the user’s privileges. Seven thousand public servers and 150 million downloads sit on this pattern, with CVEs already issued for LiteLLM, Flowise, Windsurf, DocsGPT, Bisheng, Agent Zero, and Upsonic.

Anthropic’s response is that the behavior is “expected.” They’re not wrong about the protocol. They are wrong about what most teams should be running in production.

The footgun

The reference SDK exposes StdioTransport(command=..., args=[...]). If your client wires config data straight into that constructor, the config becomes an RCE primitive. A single untrusted string (a tool description the attacker influenced, a marketplace entry the user installed, a commit in a repo the LLM read) ends up in command, and the transport runs it.

This is the same class of bug the industry solved twenty years ago for shell injection and ten years ago for SSRF. Take untrusted input, hand it to a system primitive, find out what the primitive was willing to do. The fix is always the same: structure the input, narrow the surface, don’t let the user-facing layer reach the dangerous API.

Four attack vectors

OX Security grouped the exploitation paths into four patterns.

The first is direct command injection via MCP STDIO: unauthenticated access to an MCP server endpoint that exposes the raw command field. The second is STDIO config bypassing hardening, where local config files accept arbitrary command strings and any server-side hardening doesn’t apply. The third is zero-click prompt injection rewriting mcp.json, where an LLM with file access is coaxed into modifying the client’s MCP config so the next session launches the attacker’s binary. The fourth is hidden STDIO triggers through MCP marketplaces, where an open marketplace entry ships a server definition whose command runs on install or first use.

Each variant has a slightly different prerequisite. All four end the same way: the attacker’s process, running under the user’s identity.

How Runlayer breaks each one

Runlayer is the enterprise MCP platform. The architecture assumes the LLM and the config file are untrusted inputs, not trusted principals. That assumption cascades through every layer.

Command allowlist at the control plane. ServerCreate and ServerUpdate both run validate_stdio_command against an allowlist of npx, uvx, and uv. You cannot persist a Server row whose command launches bash -c or curl | sh. If an attacker drops a poisoned row into the database directly, the validator rejects it before it reaches any executor.

Structured argv, not a shell string. The TransportConfigInput API takes command and args as separate fields. There is no string concatenation, no sh -c, no operator interpretation. The child process runs with the exact argv vector we validate, nothing more.

Curated catalog, not open upload. The Runlayer catalog is schema-validated and reviewed. Marketplace poisoning as a class requires an open upload surface. We don’t have one.

Client-side enforcement via hooks. For Cursor, Runlayer ships a beforeMCPExecution hook that checks is_runlayer_managed_command. If an LLM is prompt-injected into rewriting mcp.json to point at an attacker binary, the hook fails closed. The executor never launches it. This is the specific mitigation for the zero-click variant: the LLM can scribble on the config all it wants, because the hook refuses to run anything that didn’t come through the Runlayer control plane.

Verified local proxy for signed local MCPs. Local MCP servers run through a verified proxy that validates the signed manifest before exec. Unsigned binaries don’t launch, which closes the social-engineering-a-local-install vector.

Audit logging on every tool invocation. Every MCP call, every argv vector, every hook decision is logged. If anything does slip through, the forensic trail is already there.

Mapping this back to the four vectors: direct command injection is caught at the API by the allowlist and the structured argv. STDIO config bypassing hardening is caught by the client hook (on Cursor). Zero-click prompt injection rewriting the config is caught by the client hook and by the fact that Runlayer LLMs don’t persist to Server rows. Marketplace poisoning is caught by the curated catalog.

The honest limits

Two things Runlayer does not fully solve.

Supply-chain trust via package names is still an npm and PyPI problem. If npx some-popular-package is on your allowlist and a maintainer pushes a malicious release, the allowlist won’t save you. We recommend pinning packages and restricting the allowlist to a curated subset. We can help enforce that at the control plane, but the root of trust is upstream.

Non-Cursor clients that don’t implement hooks degrade the client-side enforcement story. The control plane protections still hold, so poisoned Server rows never run. The hook-based defenses against config rewriting don’t apply until the client implements the equivalent. We’d rather be specific about the threat model than hand you a compliance slide.

The takeaway

If you’re running MCP in production, the question isn’t whether the protocol is safe. It’s whether your client takes raw command strings from anywhere the LLM can touch. If the answer is yes, you have the bug OX wrote up, regardless of which CVE they’ve assigned to whichever implementation you’re running.

The answer isn’t to wait for Anthropic to change the spec. It’s to put MCP behind a gateway that doesn’t take raw command strings.

That’s what Runlayer does.

If you operate MCP at enterprise scale, book a demo and we’ll walk through the control plane, the Cursor hook, and how the catalog is curated.

Related posts