GHSA-3f7w-8rr8-f37f: Path Traversal
Target: gitpython-developers/GitPython Tested: HEAD 07e80555 (2026-07-25), latest release 3.1.55, git version 2.50.1 Reported instances: 2 exploitable, from a sweep of 14 unguarded call sites
Summary
GitPython blocks dangerous git options through Git.checkunsafeoptions(), gated per method by an allowunsafeoptions parameter. That guard is applied per call site, so any API that forwards kwargs into a git command without calling it passes caller-controlled options straight to git.
A mechanical sweep of every method that forwards kwargs into a .git.<command>(...) call found 14 sites with no guard. Two reach a git option that takes a filesystem path:
| # | Call site | git option | Impact | |---|---|---|---| | 1 | IndexFile.checkout() → git checkout-index | --prefix=<path> | arbitrary file overwrite with repository-controlled content | | 2 | TagReference.create() → git tag | -F <file> / --file=<file> | arbitrary file read, returned in-band |
This is the same defect class already fixed in Commit.count() (GHSA-p538-c434-8v24), Repo.archive() and Git.lsremote() (GHSA-956x-8gvw-wg5v). Both instances below are still present at HEAD.
---
Instance 1 — IndexFile.checkout(): arbitrary file overwrite
git/index/base.py:1210 accepts kwargs and forwards them with no guard:
python def checkout(self, paths=None, force=False, fprogress=lambda args: None, kwargs): ... proc = self.repo.git.checkoutindex(args, kwargs) # line 1331 ... proc = self.repo.git.checkoutindex(args, kwargs) # line 1349
There is no allowunsafeoptions parameter and no checkunsafeoptions() call in the method.
git checkout-index accepts --prefix=<string>, prepended to every output path. It is not confined to the working tree, so an absolute prefix writes tracked file contents anywhere the process can write, and -f overwrites what is already there.
Reproduction
python from git import Repo Repo("/path/to/repo").index.checkout(prefix="/tmp/targetdir/", a=True, f=True)
Observed (poc/poccheckoutindex.py) — no exception raised, files land outside the repository:
[ALLOWED] no UnsafeOptionError raised files written outside the repo: ['f.txt'] f.txt: 'hi\n'
Overwrite of a pre-existing file (poc/poccioverwrite.py) — the victim file held ORIGINAL-DO-NOT-CLOBBER\n before the call:
[ALLOWED] no exception victim content now: 'hi\n' OVERWRITTEN: True
Why this rates High
Both halves of the write are attacker-influenced:
- Destination — the prefix kwarg. - Content — the bytes written are repository blobs, so anyone who can land a file in the repository (a pull-request branch, a mirrored or untrusted repository, an agent-cloned repository) controls exactly what is written.
Commit a file named authorizedkeys, .bashrc, config or post-checkout, choose the matching prefix (~/.ssh/, ~/, .git/hooks/), and the write becomes code execution as the service account.
For comparison within this project: GHSA-fjr4-x663-mwxc (arbitrary file overwrite via git diff --output) is rated High, and GHSA-p538-c434-8v24 (arbitrary file truncation via git rev-list --output) is rated Medium. --prefix supplies full content control, so it sits at or above the former.
---
Instance 2 — TagReference.create(): arbitrary file read
git/refs/tag.py:88 forwards kwargs into git tag with no guard, and the signature advertises the passthrough:
python def create(cls, repo, path, reference="HEAD", logmsg=None, force=False, kwargs): """... :param kwargs: Additional keyword arguments to be passed to :manpage:git-tag(1). """
git tag accepts -F <file> / --file=<file>, which reads the tag message from an arbitrary path. The annotated tag object stores that content and GitPython returns it to the caller via TagReference.tag.message, so the file contents come back in-band.
Reproduction
python from git import Repo from git.refs.tag import TagReference
t = TagReference.create(Repo("/path/to/repo"), "x", force=True, a=True, F="/etc/passwd") print(t.tag.message)
Observed (poc/poctagF.py), reading a canary file outside the repository:
[ALLOWED] no UnsafeOptionError raised >>> tag message recovered from arbitrary path: 'TAG-READ-CANARY-98765\nsecond-line-secret'
Impact is a read at the privileges of the process. I am not claiming code execution for this instance. The signing options (-s, -u/--local-user) do invoke gpg from the same unguarded kwargs, but I did not develop that into command execution and make no claim about it.
---
Sweep results — the other 12 sites
Reported so the fix can be scoped once rather than per report. poc/sweep.py reproduces this list.
| Call site | git command | Assessment | |---|---|---| | IndexFile.fromtree() | read-tree | --index-output=<path> looked reachable but is neutralised: GitPython appends its own --index-output after the caller's kwargs and git honours the last occurrence. Verified — victim file unchanged (poc/pocreadtree.py) | | IndexFile.remove() | rm | --pathspec-from-file only reads a pathspec; no write or disclosure primitive found | | IndexFile.move() | mv | same | | HEAD.reset() | reset | same | | HEAD.checkout() | checkout | same | | Head.delete(), RemoteReference.delete() | branch | no path-taking option found | | Repo.mergebase() | merge-base | no path-taking option found | | Repo.getuntrackedfiles() | status | no path-taking option found | | Remote.seturl(), Remote.create(), Remote.update() | remote | URL handling already addressed by GHSA-94p4-4cq8-9g67 |
Suggested remediation
Immediate: add allowunsafeoptions: bool = False to both methods and gate Git.optioncandidates(args, kwargs) against new lists — unsafegitcheckoutindexoptions = ["--prefix"] (consider --temp) and unsafegittagoptions = ["--file", "-F"] (consider -s, -u/--local-user, --cleanup) — matching the pattern used in Repo.archive() and Commit.count().
Structural: this defect has now been fixed four times in four places (Repo.archive(), Git.lsremote(), Commit.count(), and the two here), because the guard is opt-in per method: every new kwargs-forwarding API starts unguarded and stays that way until someone reports it. Enforcing the check centrally in Git.callprocess() — each git invocation consults a per-command unsafe-option table unless the caller opts out — would make new call sites safe by default rather than by review, and would close the remaining sites in the table above at the same time.
Disclosure
Reported privately via GitHub private vulnerability reporting.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/GitPythonto a version that resolves this vulnerability.Fixed in 3.1.57 - Configuration
Enforce the unsafe-option check centrally in Git._call_process(): each git invocation consults a per-command unsafe-option table unless the caller opts out, so new **kwargs-forwarding call sites are safe by default (this closes the remaining unguarded sites, including IndexFile.checkout() and TagReference.create()).
gitpython Git._call_process() unsafe option gating (per-command unsafe-option table) = enabled-by-default - Configuration
Add allow_unsafe_options: bool = False to IndexFile.checkout() and gate Git._option_candidates(args, kwargs) against unsafe_git_checkout_index_options = ["--prefix"] (consider "--temp"), matching the pattern used in Repo.archive() and Commit.count().
IndexFile.checkout() allow_unsafe_options = False - Configuration
Add allow_unsafe_options: bool = False to TagReference.create() and gate Git._option_candidates(args, kwargs) against unsafe_git_tag_options = ["--file", "-F"] (consider "-s", "-u"/"--local-user", "--cleanup"), matching the pattern used in Repo.archive() and Commit.count().
TagReference.create() allow_unsafe_options = False - Configuration
Ensure TagReference.create() no longer forwards caller-controlled kwargs into git tag without calling Git.check_unsafe_options(); the method currently has no allow_unsafe_options parameter and no check_unsafe_options() call, which allows -F/--file to read arbitrary filesystem content and return it in-band via TagReference.tag.message.
TagReference.create() git tag passthrough of kwargs = guarded
Event History
Frequently Asked Questions
What is the severity of GHSA-3f7w-8rr8-f37f?
The severity of GHSA-3f7w-8rr8-f37f is high with a score of 8.1.
How do I fix GHSA-3f7w-8rr8-f37f?
You can fix GHSA-3f7w-8rr8-f37f by updating to the latest version of GitPython, specifically version 3.1.56 or higher.
What vulnerabilities are associated with GHSA-3f7w-8rr8-f37f?
GHSA-3f7w-8rr8-f37f is associated with vulnerabilities including Path Traversal and Information Leak.
What affected software is tied to GHSA-3f7w-8rr8-f37f?
The affected software for GHSA-3f7w-8rr8-f37f is GitPython, which is installed via pip.
What are the potential impacts of GHSA-3f7w-8rr8-f37f?
The potential impacts of GHSA-3f7w-8rr8-f37f include unauthorized information leakage and possible file system access.