GitHub Advisoryadvisory · PyPI
GHSA-23mf-xhv8-69c2 GitPython: arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of CVE-2026-78679)
HIGH
Official page ↗
- Ecosystem
- PyPI
- Packages
- GitPython
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-200, CWE-73, CWE-88
- Reviewed
- 2026-10-27
Affected packages
• PyPI GitPython: < 3.2.0 → fixed in 3.2.0
Description
> A note on the referenced files. This report was submitted through GitHub's private
> vulnerability reporting API, which carries no attachments. Every transcript this report leans
> on is reproduced inline above, verbatim. The file names it mentions (`poc.sh`, `poc.log`,
> per-case transcripts, `evidence.md`, `negative-controls.md`, blob-sweep tables) are the local
> evidence bundle those inline excerpts were extracted from, with a checksum manifest. I am happy
> to send the bundle by any channel you prefer, or to open a private fork — just say which. The
> PoC scripts are self-contained and reproduce the result from a clean environment.
---
Affected: ` "The audit also found that reset's positional commit could carry the option before its argument
> separator. […] Check reset's positional commit as well as keyword options"
`HEAD.reset` got the fix. `Head.checkout` did not, and `1b0d2d9b` did not come back for it.
Affected Asset
| | |
|---|---|
| Package | `GitPython` (PyPI) |
| Affected versions | **` List[str]:
"""Collect possible option spellings before command-line transformation."""
options = [
option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-")
]
```
So `_option_candidates([], kwargs)` means: *check the keyword arguments, never check the positional
arguments this method puts on the command line.* That is fine when the method has no
attacker-influenced positional, or when it separates them with `--`.
`git/refs/head.py:283-293` — `Head.checkout()` has neither:
```python
if not allow_unsafe_options:
Git.check_unsafe_options(
options=Git._option_candidates([], kwargs),
unsafe_options=Git.unsafe_git_pathspec_from_file_options,
)
kwargs["f"] = force
if kwargs["f"] is False:
kwargs.pop("f")
self.repo.git.checkout(self, kwargs)
```
`self` is the only positional. It serialises to the branch name, lands in the `git checkout` argv,
and is not in the list the guard inspects. `_call_process` builds `args_list = opt_args + ext_args`,
so keyword-derived options precede positionals — which is why the rest of the library puts `"--"`
first (`Remote.fetch/pull/push`, `Repo._clone`, `Repo.archive`, `IndexFile.remove`, `Commit.count`
all do). `Head.checkout` does not.
Why the branch name is attacker-controlled
Git accepts a refname whose branch component begins with `-` and contains `/`
(`raw/02-refname-accepted-and-cloned.txt`):
```
>>> git check-ref-format 'refs/heads/--pathspec-from-file=/etc/passwd'
exit=0 stderr=''
=> un refname que empieza con '-' y lleva '/' es VALIDO para git
>>> git -C update-ref 'refs/heads/--pathspec-from-file=/etc/passwd' beea87239099
exit=0 stderr=''
>>> git -C symbolic-ref HEAD 'refs/heads/--pathspec-from-file=/etc/passwd'
exit=0
HEAD del bare = refs/heads/--pathspec-from-file=/etc/passwd
ref en disco como arbol jerarquico: True
>>> Repo.clone_from('file://', ) # la app solo clona
ramas LOCALES creadas por el clone, sin que nadie las pida:
refs/heads/--pathspec-from-file=/etc/passwd
repo.active_branch.name = '--pathspec-from-file=/etc/passwd'
coincide con el nombre hostil: True
```
The transcript above also shows the second half: `git clone` materialises that branch locally on
its own** when the remote `HEAD` points at it — the consuming application never asks for it.
A plain git server accepts the refname over `receive-pack`, with no `--force` and no special config
(`raw/06-push-accepts-refname.txt`), so the attacker does not need filesystem ac…
Related
References
- https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-23mf-xhv8-69c2 web
- https://github.com/gitpython-developers/GitPython/pull/2233 web
- https://github.com/gitpython-developers/GitPython/commit/8da65080849a56a2fc9f3e7817bbf0524e8397d5 web
- https://github.com/gitpython-developers/GitPython package
- https://github.com/gitpython-developers/GitPython/releases/tag/3.2.0 web