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

FieldDetail
Filekubectl_mcp_tool/minimal_wrapper.py
Functionrun_kubectl_command(command) (starts at line 128)
Affected Versions<= 1.1.1
Fixed Version1.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 kubectl to 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():

  1. Takes user-controlled input,
  2. Forms a command string by string interpolation, and
  3. 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=True from 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

DateEvent
2025-12-12Vulnerability discovered
2025-12-15Vendor notified via GitHub issue (#36, private contact requested)
2025-12-17CVE reported to MITRE
2026-01-19Patch released (v1.2.0, PR #40 merged)
2026-02-20CVE-2025-69902 reserved
2026-03-16Public disclosure

9. References