Skip to content

Commit 8da6508

Browse files
Byroncodex
andcommitted
fix: validate checkout positional reference options
<!-- agent --> Head.checkout checked keyword options but omitted the serialized reference from its existing unsafe-option validation (GHSA-23mf-xhv8-69c2). Reference names can originate in a cloned repository, so callers could reach behavior that normally requires explicit opt-in without supplying any checkout options themselves. Pass self through the shared option-candidate helper, matching the argument actually sent to Git. This applies the existing policy to direct and cloned references, including abbreviated option spellings, while retaining allow_unsafe_options=True and ordinary checkout semantics. A leading -- separator would instead make the reference a pathspec and break branch switching. Add direct-reference regression coverage and a local clone regression using synthetic file content; the latter also verifies explicit opt-in. Both regression tests failed before the guard change. All 30 tests in test/test_refs.py pass with Python 3.12.14 and Apple Git 2.50.1; git diff --check passes. Git behavior reference: local git/git checkout at 1630431f326e15fcde608827b5ff38422528eb59, builtin/checkout.c checkout_main pathspec_from_file handling, which parses file contents as pathspecs. No Git source was copied. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
1 parent fb7e089 commit 8da6508

3 files changed

Lines changed: 21 additions & 1 deletion

File tree

doc/source/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Changelog
88
Security fixes for
99

1010
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-gq48-pqfc-9p58
11+
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-23mf-xhv8-69c2
1112

1213
If you can, also try and provide feedback on the upcoming v4 branch
1314
https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.

git/refs/head.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def checkout(
283283
"""
284284
if not allow_unsafe_options:
285285
Git.check_unsafe_options(
286-
options=Git._option_candidates([], kwargs),
286+
options=Git._option_candidates([self], kwargs),
287287
unsafe_options=Git.unsafe_git_pathspec_from_file_options,
288288
)
289289
kwargs["f"] = force

test/test_refs.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,25 @@ def test_head_checkout_rejects_pathspec_from_file(self, rw_repo):
289289
pathspec_file_nul=True,
290290
**{option_name: str(pathspecs)},
291291
)
292+
for option_name in ("--pathspec-from-file", "--pathspec-from"):
293+
branch = Head(rw_repo, f"refs/heads/{option_name}={pathspecs}")
294+
with self.assertRaises(UnsafeOptionError):
295+
branch.checkout()
296+
297+
def test_cloned_head_checkout_rejects_pathspec_from_file(self):
298+
with tempfile.TemporaryDirectory() as tdir:
299+
base_dir = Path(tdir)
300+
with self._repo_with_initial_commit(base_dir) as source:
301+
branch = source.create_head("--pathspec-from-file=pathspecs")
302+
source.head.reference = branch
303+
with Repo.clone_from(source.working_tree_dir, base_dir / "clone") as cloned:
304+
(base_dir / "clone" / "pathspecs").write_text("unmatched-private-content\n", encoding="utf-8")
305+
assert cloned.active_branch.name == branch.name
306+
with self.assertRaises(UnsafeOptionError):
307+
cloned.active_branch.checkout()
308+
with self.assertRaises(GitCommandError) as error:
309+
cloned.active_branch.checkout(allow_unsafe_options=True)
310+
assert "unmatched-private-content" in str(error.exception)
292311

293312
@with_rw_repo("HEAD")
294313
def test_head_reset_rejects_pathspec_from_file(self, rw_repo):

0 commit comments

Comments
 (0)