Feb 23, 2026·5 min read·13 visits
The terminal-controller-mcp package, designed to give AI models terminal access, attempted to block dangerous commands like 'mkfs' using a simple string blacklist. This CVSS 10.0 vulnerability allows attackers to bypass that check using standard shell features (e.g., specific character concatenation), leading to full Remote Code Execution (RCE).
A critical command injection vulnerability in terminal-controller-mcp allows attackers to bypass keyword-based filters and execute arbitrary code. By leveraging shell command substitution, malicious actors can reconstruct forbidden commands at runtime, rendering the application's blacklist security mechanism useless.
The Model Context Protocol (MCP) is the new hotness in the AI world. It’s essentially a standardized way for Large Language Models (LLMs) to interface with local tools, databases, and—in this specific case—your system terminal. The terminal-controller-mcp package does exactly what it says on the tin: it exposes a server that allows an AI (or anyone controlling the prompt) to execute shell commands on the host machine.
From a security perspective, this is the digital equivalent of handing a loaded shotgun to a toddler and hoping they don't pull the trigger. The premise of the tool requires exec capabilities by design. The developers knew this was risky, so they implemented a safety mechanism. They decided to act as a nanny, filtering out "bad words" before passing the command to the system shell.
Unfortunately, as history has taught us time and time again, trying to secure a shell interpreter by grepping for bad strings is a fool's errand. It’s not just a vulnerability; it’s a fundamental misunderstanding of how command shells parse input.
The core issue lies in the execute_command function. The developers implemented a blacklist—a list of specific strings that are strictly forbidden. If you try to run rm -rf / or mkfs, the Python code spots the substring, wags its finger at you, and returns a security warning.
Here is the logic flaw: Python sees a string of characters, but the Shell sees a programming language. Python checks the input statically before execution. The Shell evaluates the input dynamically during execution. If you can make the input look safe to Python but dangerous to the Shell, you win.
This is a classic "Time of Check to Time of Use" (TOCTOU) logical gap, but specifically regarding parsing context. The Python filter is looking for the literal sequence m-k-f-s. It does not understand that in Bash, $(echo mkfs) is semantically identical to mkfs.
Let's look at the vulnerable code in terminal_controller.py. It’s short, readable, and dangerously naive. It effectively tries to secure the system by checking if a substring exists within the user input.
# Inside terminal_controller.py
def execute_command(self, command: str) -> str:
# The fatal mistake: A static blacklist
dangerous_commands = ["rm -rf /", "mkfs"]
# The check
if any(dc in command.lower() for dc in dangerous_commands):
return "For security reasons, this command is not allowed."
try:
# The execution
result = subprocess.run(
command,
shell=True, # <--- The root of all evil
capture_output=True,
text=True,
timeout=timeout
)
return result.stdout
except Exception as e:
return str(e)The presence of shell=True is the final nail in the coffin. It tells Python to pass the entire string to /bin/sh (or cmd.exe) for parsing. This enables piping, redirection, and variable expansion—features that the simple if statement above completely ignores.
To exploit this, we don't need buffer overflows or heap spraying. We just need to be creative with string concatenation. We want to execute mkfs (make file system) to wipe a drive, but we can't type "mkfs".
We can use shell command substitution $() to construct the command character by character. The Python filter will see a bunch of echo commands, which aren't on the blacklist. The Shell, however, will execute the inner commands first, assemble the string, and then execute the result.
The Attack Payload:
echo "$($(echo -n m; echo -n k; echo -n f; echo -n s))"The Execution Flow:
$().echo -n m, echo -n k, etc. The result is the string mkfs.This technique is robust and works against almost any keyword-based filter that doesn't account for shell meta-characters.
Because terminal-controller-mcp is designed to run shell commands, the impact of bypassing the filter is effectively total system compromise. The severity is rated Critical (10.0) for a reason.
If this package is running inside a Docker container, the attacker has root inside that container. If the container is privileged or shares mounts with the host, escape is likely. If the user installed this locally to let their AI assistant "help with coding," the attacker now has the same permissions as that developer—access to SSH keys, source code, and AWS credentials.
This isn't just about wiping drives with mkfs. An attacker can just as easily construct a reverse shell using the same obfuscation techniques to maintain persistent access without ever triggering the "dangerous command" filter.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
terminal-controller-mcp GongRzhe | <= 0.1.7 | TBD |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-78 (OS Command Injection) |
| CVSS v3.1 | 10.0 (Critical) |
| Attack Vector | Network |
| Privileges Required | None |
| Exploit Status | Proof-of-Concept Available |
| EPSS Score | 0.00682 |
The software constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.