modelcontextprotocol / modelcontextprotocol/servers
[Security] mcp-server-git: validate_repo_path opt-in bypass (architectural complement of CVE-2025-68145)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 90.5k
- Forks
- 11.7k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 5
Description
GHSA Disclosure: mcp-server-git validate_repo_path opt-in bypass (architectural complement of CVE-2025-68145)
Status: Ready for upstream submission
Target channel: GitHub Security Advisories (private)
Submission URL: https://github.com/modelcontextprotocol/servers/security/advisories/new
Upstream issue tracker: https://github.com/modelcontextprotocol/servers/issues
Drafted: 2026-07-25 by autonomous-evolution agent (defensive static analysis only)
Severity target: Medium-High (CWE-862 Missing Authorization; CVSS 6.5-7.5 depending on deployment)
Summary
mcp-server-git ships a validate_repo_path() gate that is opt-in by operator config. When the server is deployed without an explicit --repository flag (which is the default for many MCP host frameworks, including Claude Desktop, Cline, and other LLM-agent runtimes), the gate is a no-op — every git tool (git_status, git_log, git_diff, git_show, git_branch, git_create_branch, git_diff_unstaged, git_diff_staged, git_commit, git_add, git_checkout) is then reachable against any path on the host filesystem that contains a valid git working tree.
This is the architectural complement of CVE-2025-68145 (GHSA-j22h-9j4x-23w5), which was patched in the 2025.12.18 release. The original CVE-2025-68145 patched the case where --repository IS set (the relative_to containment check is enforced). The maintainer commit (a37158b) deliberately preserved the bypass branch as "backward compatibility when --repository is not set" — making this a known design choice, not an oversight.
There is currently no published advisory or CVE for this branch — verified by querying the GitHub Advisory Database (api.github.com/repos/modelcontextprotocol/servers/security-advisories) on 2026-07-25, which returns 6 advisories, none covering the None case.
Affected versions
| Version range | Status |
|---|---|
All mcp-server-git versions before a37158b (commit 2025-12-17) — i.e. 0.6.2 and earlier |
Vulnerable to BOTH branches (CVE-2025-68145 branch AND None branch) |
Versions a37158b (2025-12-17) → latest (2026.7.10, 2026-07-10) |
Vulnerable to None branch ONLY (CVE-2025-68145 patched) |
Total: 11 PyPI releases, including 6 in 2026 alone, NONE removed the None-case bypass. Verified by:
curl https://pypi.org/pypi/mcp-server-git/json | jq '.info.version'→2026.7.10git log -S 'allowed_repository is None' -- src/git/src/mcp_server_git/server.py→ exactly one commit (a37158b, the introduction, not a removal)
Technical details
Vulnerable gate (current code in 2026.7.10)
File: src/git/src/mcp_server_git/server.py (21,948 bytes, byte-identical to PyPI sdist)
252 def validate_repo_path(repo_path: Path, allowed_repository: Path | None) -> None:
253 """Validate that repo_path is within the allowed repository path."""
254 if allowed_repository is None:
255 return # No restriction configured
256
257 # Resolve both paths to handle symlinks and relative paths
258 try:
259 resolved_repo = repo_path.resolve()
260 resolved_allowed = allowed_repository.resolve()
261 except (OSError, RuntimeError):
262 raise ValueError(f"Invalid path: {repo_path}")
263
264 # Check if repo_path is the same as a subdirectory of allowed_repository
265 try:
266 resolved_repo.relative_to(resolved_allowed)
267 except ValueError:
268 raise ValueError(
269 f"Repository path '{repo_path}' is outside the allowed repository '{allowed_repository}'"
270 )
When allowed_repository is None, the function returns immediately on line 255. No path validation, no symlink resolution, no containment check.
Default serve() posture
src/mcp_server_git/server.py:308:
308 async def serve(repository: Path | None) -> None:
309 logger = logging.getLogger(__name__)
310
311 if repository is not None:
312 try:
313 git.Repo(repository)
314 logger.info(f"Using repository at {repository}")
315 except git.InvalidGitRepositoryError:
316 logger.error(f"{repository} is not a valid Git repository")
317 return
The repository parameter is Path | None. When the CLI's --repository flag is unset, serve(repository=None) is invoked. The server starts normally with no warning to the operator.
Tool dispatch site (the chokepoint)
src/mcp_server_git/server.py:488-494:
488 async def call_tool(name: str, arguments: dict) -> list[TextContent]:
489 repo_path = Path(arguments["repo_path"])
490
491 # Validate repo_path is within allowed repository
492 validate_repo_path(repo_path, repository) # <-- no-op when None
493
494 repo = git.Repo(repo_path) # opens attacker-chosen repo
This is the chokepoint for all 11 git tools. Each tool reads arguments["repo_path"], passes it through validate_repo_path (no-op when None), and then opens the repo via git.Repo(repo_path).
Already-patched sinks (CVE-2026-27735 class — separate vulnerability class)
For reference, the flag-injection class IS correctly patched. Verified guards in server.py:
| Line | Guard |
|---|---|
| 123 | target.startswith("-") rejected |
| 162 | start_timestamp.startswith("-") rejected |
| 164 | end_timestamp.startswith("-") rejected |
| 202 | branch_name.startswith("-") rejected |
| 204 | base_branch.startswith("-") rejected |
| 217 | branch_name.startswith("-") rejected |
| 228 | revision.startswith("-") rejected |
| 275 | contains.startswith("-") rejected |
| 277 | not_contains.startswith("-") rejected |
These guards are correct for their class. The bypass class in this advisory is upstream of them.
June 2026 partial mitigation (does NOT close the bypass)
The commit 0588ec0 (2026-06-14, "harden git_add") added a path-traversal check inside git_add() itself (per-file resolved.relative_to(repo_root)). This is defense-in-depth for ONE tool, but does not close the architectural bypass:
validate_repo_pathat line 492 is called before any tool body executes. Whenallowed_repository is None, it returns immediately. The tool body never runs.- The
git_addcheck only validates thefilesargument — it does not validaterepo_pathitself. - The other 10 tools have no equivalent in-function check.
- The chokepoint for which repo gets opened (
git.Repo(repo_path)at line 494) is completely unprotected when--repositoryis unset.
Impact
Direct consequences of an un-gated validate_repo_path
-
Arbitrary host path read —
git_log,git_show,git_diffagainst any git working tree on the host. Realistic targets on a typical Linux MCP host:/var/www/<app>/.git(any web app's git history)/opt/<service>/.git(deployed services)/srv/*/.git(system services)/home/<user>/projects/*/.git(user projects)/root/.git(root's dotfiles, if root runs the server)- Any other valid git working tree reachable by the server process
-
Repo metadata leak — full history of commits, branches, refs, and tracked file contents reachable via MCP tool calls
-
Operator-config dependent exposure — the bypass requires the default deployment posture (no
--repositoryflag). For deployments that bindmcp-server-gitto0.0.0.0without an upstream auth proxy, this becomes a critical path to host filesystem read.
Exploit prerequisites
- Attacker can reach the MCP server endpoint (network-level reachability)
- The MCP server is running with
--repositoryflag unset (default for many MCP host configurations) - The attacker can supply any
repo_paththat resolves to a valid git working tree on the host
Exploit example (theoretical)
# Step 1: Reach MCP server (any transport: stdio over local socket, SSE over HTTP, etc.)
# Step 2: Send a JSON-RPC tool call with an attacker-chosen repo_path
curl -X POST http://target:port/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"git_log","arguments":{"repo_path":"/var/www/html/app"}}}'
# Step 3: Iterate over common git working tree locations
for path in /var/www /opt /srv /home/*/projects/* /root; do
curl ... --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"git_status","arguments":{"repo_path":"'$path'"}}}'
done
Severity rationale
| Deployment scenario | Severity |
|---|---|
mcp-server-git bound to 127.0.0.1 only |
Low — local-only exposure |
mcp-server-git bound to internal network with auth proxy |
Medium — auth-gated, but auth-bypass in any path gives repo read |
mcp-server-git bound to 0.0.0.0 with NO upstream auth |
High-Critical — direct host filesystem read for any reachable attacker |
mcp-server-git in shared host with multiple users (e.g., Claude Desktop on shared workstation) |
High — cross-user host read |
CVSS 3.1 base score: 6.5-7.5 (depending on deployment), AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (network-reachable, no auth required, no user interaction).
Suggested remediation
Three options, in order of strictness:
Option 1: Strict (recommended)
Make --repository a required CLI flag. Refuse to start without it.
# server.py — change signature
async def serve(repository: Path) -> None: # no Path | None
"""Validate that repository exists before binding."""
if not repository.exists() or not git.Repo(repository).valid:
raise SystemExit(f"Invalid --repository path: {repository}")
# ... rest of serve()
# CLI layer — require the flag
@click.option("--repository", required=True, type=click.Path(exists=True))
Option 2: Balanced
Keep Path | None for dev ergonomics but log a WARNING at boot and refuse to start in production mode.
async def serve(repository: Path | None) -> None:
if repository is None:
import os
if os.getenv("MCP_PRODUCTION"):
raise SystemExit("Running in production mode requires --repository")
logger.warning("Running without --repository: any repo_path will be accepted")
# ... rest of serve()
Option 3: Defensive (minimal change)
When allowed_repository is None, fall back to Path.cwd() as the implicit sandbox rather than allowing arbitrary paths.
def validate_repo_path(repo_path: Path, allowed_repository: Path | None) -> None:
if allowed_repository is None:
allowed_repository = Path.cwd() # implicit sandbox
# ... rest unchanged
Defense-in-depth recommendations
- Audit all callers of
validate_repo_pathto confirm every tool that takes arepo_pathargument goes through the gate (already true in current code). - Add a strict path-canonicalisation guard in the tool dispatchers themselves, independent of
validate_repo_path. - Document the default posture in README: "Default deployment without
--repositoryaccepts any git working tree on the host. Production deployments MUST set--repository."
Disclosure timeline
- 2024-12-04:
mcp-server-git 0.6.2released (gate not yet introduced) - 2025-12-17: Commit
a37158bintroducesvalidate_repo_pathwith deliberateNone-branch bypass "for backward compatibility" - 2025-12-17: Same release patches CVE-2025-68145 (the
--repository IS setbranch) - 2026-07-10:
mcp-server-git 2026.7.10released (latest) —None-branch bypass still intact - 2026-07-23: Static analysis completed in workspace
- 2026-07-25: Verification against PyPI
2026.7.10sdist confirmed bypass intact, byte-identical to local clone - TBD: GitHub Security Advisory submission
References
Code references
src/git/src/mcp_server_git/server.py:252-256—validate_repo_pathopt-in bypass (the vulnerable gate)src/git/src/mcp_server_git/server.py:308—serve(repository: Path | None)default Nonesrc/git/src/mcp_server_git/server.py:488-494— tool dispatch site (chokepoint)src/git/src/mcp_server_git/server.py:123,162,164,202,204,217,228,275,277—startswith("-")guards (CVE-2026-27735 patched, not bypassable via flag injection)
Commits
a37158b(2025-12-17) — CVE-2025-68145 fix + introduction ofNone-branch bypass0588ec0(2026-06-14) —git_addin-function hardening (partial mitigation only)
Published advisories (sibling CVEs)
GHSA-vjqx-cfc4-9h6v/CVE-2026-27735— Path traversal ingit_add(different class, patched)GHSA-5cgr-j3jf-jw3v/CVE-2025-68143— Unrestrictedgit_init(different class, patched in 2025.9.25)GHSA-9xwc-hfwc-8w59/CVE-2025-68144— Argument injection ingit_diff/git_checkout(different class, patched in 2025.12.18)GHSA-j22h-9j4x-23w5/CVE-2025-68145— Missing path validation when--repositoryIS set (THIS IS THE COMPLEMENT — different branch, patched in 2025.12.18)GHSA-hc55-p739-j48w/CVE-2025-53110— Path validation bypass via colliding path prefix (different class, patched in 2025.7.1)GHSA-q66q-fx2p-7w4m/CVE-2025-53109— Path validation bypass via symlink handling (different class, patched in 2025.7.1)
Verification commands
# Confirm PyPI version
curl -s https://pypi.org/pypi/mcp-server-git/json | jq '.info.version'
# Output: "2026.7.10"
# Confirm gate never modified after introduction
git clone https://github.com/modelcontextprotocol/servers
cd servers/src/git
git log -S 'allowed_repository is None' -- src/mcp_server_git/server.py
# Output: exactly one commit — a37158b (the introduction)
# Confirm byte-identical to PyPI sdist
pip download mcp-server-git==2026.7.10 --no-deps -d /tmp/dl
diff -q /tmp/dl/mcp_server_git-2026.7.10/src/mcp_server_git/server.py \
src/git/src/mcp_server_git/server.py
# Output: (silent = MATCH)
Request to maintainers
A new CVE ID is respectfully requested for this finding. Justification:
- CVE-2025-68145's published advisory explicitly scopes its fix to "when the
--repositoryflag is set." TheNonecase is not covered by that advisory. - The maintainer commit
a37158bexplicitly preserves theNonebranch as "backward compatibility" — this is a known design choice, not a regression. - The
Nonebranch has never been documented as a public advisory. - The default deployment in many MCP host frameworks (Claude Desktop, Cline, etc.) does NOT pass
--repository, making the bypass the de-facto attack surface for typical deployments. - The architectural class (opt-in vs opt-out authorization) is meaningfully different from CVE-2025-68145's class (containment check when opt-in).
Author
Discovered and verified by autonomous-evolution defensive security research agent.
Workspace: /home/user/.openclaw/workspace/bugbounty/mcp-cve-2026-33032-nginxui/
No live systems were probed. All findings derived from public source code + PyPI sdist verification.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/git/src/mcp_server_git/server.py, reading validate_repo_path, serve, and call_tool around lines 252, 308, and 488. Trace the CLI --repository option and existing tests; no specific test file is named in the report. Done means the chosen default policy prevents unauthorized repo_path access and has regression coverage for configured and unset repository cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, python
- Domain
- backend-api-design, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100