CVE: CVE-2025-69902
Severity: High
Affected Versions: <= 1.1.1
Fixed Version: 1.2.0
1. Summary
The minimal MCP server wrapper in kubectl_mcp_tool/minimal_wrapper.py is vulnerable to OS command injection because it executes a user-controlled command string using subprocess.run(..., shell=True, ...). This allows an attacker to append shell metacharacters (e.g., ;, &&, |) and execute arbitrary OS commands on the host.
This maps to CWE-78 (OS Command Injection).
2. Affected Component
| Field | Detail |
|---|---|
| File | kubectl_mcp_tool/minimal_wrapper.py |
| Function | run_kubectl_command(command) (starts at line 128) |
| Affected Versions | <= 1.1.1 |
| Fixed Version | 1.2.0 |
Key vulnerable lines:
- Line 132 — Prefixing attacker input into a command string:
command = f"kubectl {command}" - Line 140 — Executing attacker-controlled string via shell:
shell=True
The actual subprocess.run(command, shell=True, ...) call is visible in the source snippet.
Why this is vulnerable: Python explicitly warns that when shell=True is used, the application must correctly quote/escape metacharacters to prevent shell injection.
3. MCP Tool Entry Point (How Untrusted Input Reaches the Sink)
The MCP server registers a tool named process_natural_language. There are multiple paths where attacker-controlled text is passed into run_kubectl_command():
(A) Direct kubectl command path
If the incoming query starts with "kubectl ", the code executes it directly via run_kubectl_command(query).
(B) “Extracted kubectl command” path
If the query contains "run … command … kubectl", a regex extracts kubectl ... from the query and executes it.
These mean a malicious MCP client (or an LLM-driven tool call) can deliver a payload like kubectl get pods; <OS command> and reach subprocess.run(... shell=True ...).
4. Impact
- Primary: Arbitrary command execution on the host running the MCP server (RCE). Privilege is the same as the MCP server process (in the PoC below, root).
- Secondary: If the MCP server runs with a privileged kubeconfig, an attacker may also use
kubectlto impact the Kubernetes cluster (depending on RBAC/credentials).
5. Proof of Concept (PoC) — Reproduction Steps
The following PoC demonstrates OS command injection without requiring a working Kubernetes cluster, because the injected command runs in the shell regardless of kubectl connectivity.
Note: This PoC applies to versions <= 1.1.1 only. The vulnerability is patched in version 1.2.0.
1. Start a Python REPL inside your venv:
from kubectl_mcp_tool import minimal_wrapper
minimal_wrapper.run_kubectl_command("get pods; id >> /tmp/pwned")
2. Verify:
cat /tmp/pwned
Expected output shows the server process identity (e.g., uid=0(root) ...), proving arbitrary command execution.
Why it succeeds even without kubeconfig: With shell=True, the shell executes kubectl get pods; id >> /tmp/pwned. The injected command (id) runs independently of kubectl’s success or failure. This behavior is consistent with shell-invoked command strings.
6. Root Cause
The vulnerability exists because run_kubectl_command():
- Takes user-controlled input,
- Forms a command string by string interpolation, and
- Executes it using
subprocess.run(..., shell=True, ...)
This allows shell metacharacters to be interpreted by /bin/sh -c, enabling OS command injection (CWE-78).
7. Remediation
The patch (PR #40, merged 2026-01-19) addressed the vulnerability with a multi-layered approach:
- Removed
shell=Truefrom all subprocess calls - Added
sanitize_command_args()to block dangerous shell metacharacters - Added
validate_kubectl_command()with a whitelist of allowed kubectl subcommands - Added path traversal protection
import subprocess
# Before (vulnerable)
command = f"kubectl {command}"
result = subprocess.run(command, shell=True, ...)
# After (safe — simplified illustration)
args = ["kubectl"] + sanitize_command_args(command)
result = subprocess.run(args, shell=False, ...)
This was addressed in version 1.2.0.
8. Timeline
| Date | Event |
|---|---|
| 2025-12-12 | Vulnerability discovered |
| 2025-12-15 | Vendor notified via GitHub issue (#36, private contact requested) |
| 2025-12-17 | CVE reported to MITRE |
| 2026-01-19 | Patch released (v1.2.0, PR #40 merged) |
| 2026-02-20 | CVE-2025-69902 reserved |
| 2026-03-16 | Public disclosure |