MCPSafe.io
RegistryThreatsMethodologyDocsPricingScanSign in
MCPSafe.io

Security checks for MCP servers — public packages and private repos, fast or deep.

Legal

Privacy PolicyCookie PolicyTerms of ServiceSecurity disclosure

Resources

State of MCP SecuritySupportSystem statusMade in Germany 🇩🇪

© 2026 MCPSafe. All rights reserved.

GDPR — Privacy Policy
← Threat Catalog

Server Implementation

Unsanitized Input in MCP Tool Handler

HIGHCWE: CWE-94Rule: MCP-300

MCP tool handlers that pass unsanitized user-supplied strings directly to shell commands or database queries allow attackers to execute arbitrary code or exfiltrate data.

What it is

This is a code injection vulnerability (CWE-94) arising when user-controlled input is interpolated into an execution context — a shell invocation, SQL query, or eval call — without validation, escaping, or parameterization. The canonical instance is passing a user string to subprocess.run() with shell=True, which instructs the OS shell to interpret metacharacters like semicolons, pipes, and backticks as command separators. Any input reaching that call site is treated as trusted shell syntax.

Why it matters for MCP

MCP servers expose tool handlers as callable units to LLM agents, meaning a single prompt injection in retrieved content or a malicious user instruction can directly trigger tool execution with attacker-controlled arguments — no additional HTTP request or authentication step is required. The LLM layer introduces a non-deterministic trust boundary: the model may reformulate, concatenate, or pass through adversarial strings from untrusted documents as if they were legitimate tool inputs. Tool composition further amplifies risk, because the output of one tool (potentially attacker-influenced) can become the unvalidated input to a downstream tool that invokes a shell or database.

Vulnerable example

example.py
1
import mcp.server.stdio
2
import subprocess
3
4
server = mcp.server.stdio.StdioServer()
5
6
@server.tool()
7
def run_system_command(cmd: str) -> str:
8
    """Run a system command - VULNERABLE to command injection."""
9
    # No validation, shell=True allows injection
10
    output = subprocess.run(cmd, shell=True, capture_output=True, text=True)
11
    return output.stdout

Secure example

example.py
1
import mcp.server.stdio
2
import subprocess
3
import shlex
4
import re
5
6
server = mcp.server.stdio.StdioServer()
7
8
ALLOWED_COMMANDS = {"uptime", "df", "free"}
9
10
@server.tool()
11
def run_system_command(cmd: str) -> str:
12
    """Run an allowlisted system command safely."""
13
    tokens = shlex.split(cmd)
14
    if not tokens or tokens[0] not in ALLOWED_COMMANDS:
15
        raise ValueError(f"Command not permitted: {tokens[0] if tokens else '(empty)'}")
16
    output = subprocess.run(tokens, shell=False, capture_output=True, text=True)
17
    return output.stdout

How MCPSafe detects this

MCPSafe performs taint analysis tracing data flow from MCP tool handler parameters (decorated with @server.tool() or @mcp.tool()) through to sink calls including subprocess.run(), subprocess.Popen(), os.system(), and os.popen() where shell=True is set or where the argument is a string rather than a list; SQL sinks (cursor.execute(), database.execute()) are flagged when the query string is constructed via f-string interpolation or % formatting with tainted variables. Calls where the argument is a literal string constant or where the tainted value passes through an explicit allowlist membership check before the sink are excluded from findings.

See the full threat catalog for every documented detection.

Further reading

  • CWE-94: Improper Control of Generation of Code ('Code Injection')
  • OWASP: Command Injection
  • Python subprocess — Security Considerations
  • OWASP: Input Validation Cheat Sheet

Scan an MCP server for this issue

MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.

Scan now