diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py index 84188d8fd7..a9ce899e71 100644 --- a/src/git/src/mcp_server_git/server.py +++ b/src/git/src/mcp_server_git/server.py @@ -494,6 +494,12 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: # For all commands, we need an existing repo repo = git.Repo(repo_path) + if repository is not None: + if repo.working_dir: + validate_repo_path(Path(repo.working_dir), repository) + if repo.git_dir: + validate_repo_path(Path(repo.git_dir), repository) + match name: case GitTools.STATUS: status = git_status(repo) diff --git a/src/git/tests/test_server.py b/src/git/tests/test_server.py index 893195d414..ad609d13a3 100644 --- a/src/git/tests/test_server.py +++ b/src/git/tests/test_server.py @@ -339,6 +339,49 @@ def test_validate_repo_path_symlink_escape(tmp_path: Path): with pytest.raises(ValueError) as exc_info: validate_repo_path(symlink, allowed) assert "outside the allowed repository" in str(exc_info.value) + + +def test_validate_repo_path_prefix_collision(tmp_path: Path): + """Path sharing a common prefix with allowed_repository must not bypass validation.""" + allowed = tmp_path / "allowed" + allowed.mkdir() + prefix_collision = tmp_path / "allowed_suffix" + prefix_collision.mkdir() + + with pytest.raises(ValueError) as exc_info: + validate_repo_path(prefix_collision, allowed) + assert "outside the allowed repository" in str(exc_info.value) + + +def test_validate_repo_path_nested_symlink_chain(tmp_path: Path): + """Chained symlinks pointing outside allowed_repository must be rejected.""" + allowed = tmp_path / "allowed_repo" + allowed.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + + link1 = allowed / "link1" + link2 = allowed / "link2" + link2.symlink_to(outside) + link1.symlink_to(link2) + + with pytest.raises(ValueError) as exc_info: + validate_repo_path(link1, allowed) + assert "outside the allowed repository" in str(exc_info.value) + + +def test_validate_repo_path_symlink_inside_allowed(tmp_path: Path): + """Symlink pointing to a target inside allowed_repository should be accepted.""" + allowed = tmp_path / "allowed_repo" + allowed.mkdir() + real_subdir = allowed / "real_dir" + real_subdir.mkdir() + + symlink = allowed / "symlink_dir" + symlink.symlink_to(real_subdir) + + validate_repo_path(symlink, allowed) # Should not raise + # Tests for argument injection protection def test_git_diff_rejects_flag_injection(test_repository):