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.10
git 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_path at line 492 is called before any tool body executes. When allowed_repository is None, it returns immediately. The tool body never runs.
- The
git_add check only validates the files argument — it does not validate repo_path itself.
- 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 --repository is unset.
Impact
Direct consequences of an un-gated validate_repo_path
-
Arbitrary host path read — git_log, git_show, git_diff against 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 --repository flag). For deployments that bind mcp-server-git to 0.0.0.0 without 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
--repository flag unset (default for many MCP host configurations)
- The attacker can supply any
repo_path that 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_path to confirm every tool that takes a repo_path argument 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
--repository accepts any git working tree on the host. Production deployments MUST set --repository."
Disclosure timeline
- 2024-12-04:
mcp-server-git 0.6.2 released (gate not yet introduced)
- 2025-12-17: Commit
a37158b introduces validate_repo_path with deliberate None-branch bypass "for backward compatibility"
- 2025-12-17: Same release patches CVE-2025-68145 (the
--repository IS set branch)
- 2026-07-10:
mcp-server-git 2026.7.10 released (latest) — None-branch bypass still intact
- 2026-07-23: Static analysis completed in workspace
- 2026-07-25: Verification against PyPI
2026.7.10 sdist 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_path opt-in bypass (the vulnerable gate)
src/git/src/mcp_server_git/server.py:308 — serve(repository: Path | None) default None
src/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 of None-branch bypass
0588ec0 (2026-06-14) — git_add in-function hardening (partial mitigation only)
Published advisories (sibling CVEs)
GHSA-vjqx-cfc4-9h6v / CVE-2026-27735 — Path traversal in git_add (different class, patched)
GHSA-5cgr-j3jf-jw3v / CVE-2025-68143 — Unrestricted git_init (different class, patched in 2025.9.25)
GHSA-9xwc-hfwc-8w59 / CVE-2025-68144 — Argument injection in git_diff/git_checkout (different class, patched in 2025.12.18)
GHSA-j22h-9j4x-23w5 / CVE-2025-68145 — Missing path validation when --repository IS 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
--repository flag is set." The None case is not covered by that advisory.
- The maintainer commit
a37158b explicitly preserves the None branch as "backward compatibility" — this is a known design choice, not a regression.
- The
None branch 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.
GHSA Disclosure:
mcp-server-gitvalidate_repo_pathopt-in bypass (architectural complement of CVE-2025-68145)Summary
mcp-server-gitships avalidate_repo_path()gate that is opt-in by operator config. When the server is deployed without an explicit--repositoryflag (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.18release. The original CVE-2025-68145 patched the case where--repositoryIS set (therelative_tocontainment check is enforced). The maintainer commit (a37158b) deliberately preserved the bypass branch as "backward compatibility when--repositoryis 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 theNonecase.Affected versions
mcp-server-gitversions beforea37158b(commit 2025-12-17) — i.e. 0.6.2 and earlierNonebranch)a37158b(2025-12-17) → latest (2026.7.10, 2026-07-10)Nonebranch 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)When
allowed_repository is None, the function returns immediately on line 255. No path validation, no symlink resolution, no containment check.Default
serve()posturesrc/mcp_server_git/server.py:308:The
repositoryparameter isPath | None. When the CLI's--repositoryflag 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:This is the chokepoint for all 11 git tools. Each tool reads
arguments["repo_path"], passes it throughvalidate_repo_path(no-op when None), and then opens the repo viagit.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:target.startswith("-")rejectedstart_timestamp.startswith("-")rejectedend_timestamp.startswith("-")rejectedbranch_name.startswith("-")rejectedbase_branch.startswith("-")rejectedbranch_name.startswith("-")rejectedrevision.startswith("-")rejectedcontains.startswith("-")rejectednot_contains.startswith("-")rejectedThese 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 insidegit_add()itself (per-fileresolved.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.git_addcheck only validates thefilesargument — it does not validaterepo_pathitself.git.Repo(repo_path)at line 494) is completely unprotected when--repositoryis unset.Impact
Direct consequences of an un-gated
validate_repo_pathArbitrary 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)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
--repositoryflag unset (default for many MCP host configurations)repo_paththat resolves to a valid git working tree on the hostExploit example (theoretical)
Severity rationale
mcp-server-gitbound to127.0.0.1onlymcp-server-gitbound to internal network with auth proxymcp-server-gitbound to0.0.0.0with NO upstream authmcp-server-gitin shared host with multiple users (e.g., Claude Desktop on shared workstation)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
--repositorya required CLI flag. Refuse to start without it.Option 2: Balanced
Keep
Path | Nonefor dev ergonomics but log a WARNING at boot and refuse to start in production mode.Option 3: Defensive (minimal change)
When
allowed_repository is None, fall back toPath.cwd()as the implicit sandbox rather than allowing arbitrary paths.Defense-in-depth recommendations
validate_repo_pathto confirm every tool that takes arepo_pathargument goes through the gate (already true in current code).validate_repo_path.--repositoryaccepts any git working tree on the host. Production deployments MUST set--repository."Disclosure timeline
mcp-server-git 0.6.2released (gate not yet introduced)a37158bintroducesvalidate_repo_pathwith deliberateNone-branch bypass "for backward compatibility"--repository IS setbranch)mcp-server-git 2026.7.10released (latest) —None-branch bypass still intact2026.7.10sdist confirmed bypass intact, byte-identical to local cloneReferences
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
Request to maintainers
A new CVE ID is respectfully requested for this finding. Justification:
--repositoryflag is set." TheNonecase is not covered by that advisory.a37158bexplicitly preserves theNonebranch as "backward compatibility" — this is a known design choice, not a regression.Nonebranch has never been documented as a public advisory.--repository, making the bypass the de-facto attack surface for typical deployments.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.