Server Implementation
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.
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.
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.
import mcp.server.stdio |
import subprocess |
server = mcp.server.stdio.StdioServer() |
@server.tool() |
def run_system_command(cmd: str) -> str: |
"""Run a system command - VULNERABLE to command injection.""" |
# No validation, shell=True allows injection |
output = subprocess.run(cmd, shell=True, capture_output=True, text=True) |
return output.stdout |
import mcp.server.stdio |
import subprocess |
import shlex |
import re |
server = mcp.server.stdio.StdioServer() |
ALLOWED_COMMANDS = {"uptime", "df", "free"} |
@server.tool() |
def run_system_command(cmd: str) -> str: |
"""Run an allowlisted system command safely.""" |
tokens = shlex.split(cmd) |
if not tokens or tokens[0] not in ALLOWED_COMMANDS: |
raise ValueError(f"Command not permitted: {tokens[0] if tokens else '(empty)'}") |
output = subprocess.run(tokens, shell=False, capture_output=True, text=True) |
return output.stdout |
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.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now