GHSA-3f7w-8rr8-f37f: GitPython Unguarded Git Option Forwarding Vulnerability

HIGH
CVSS 8.1
CWE-73 CWE-200 CWE-22
Published
Advisory Summary

GHSA-3f7w-8rr8-f37f is a HIGH (CVSS 8.1) unguarded git option forwarding vulnerability (CWE-73: External Control of File Name or Path) affecting GitPython libraries in the PyPI ecosystem. The issue involves unguarded git option forwarding in indexfile.checkout() and tagreference.create() enables arbitrary file overwrite and arbitrary file read. Remediation is available by upgrading affected packages to version 3.1.57.

GHSA-3f7w-8rr8-f37f at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-3f7w-8rr8-f37f
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-73: External Control of File Name or Path
Affected Product GitPython
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity High (I:H)
Availability High (A:H)

How to Fix GHSA-3f7w-8rr8-f37f

To remediate this vulnerability, upgrade all affected GitPython dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

GitPython fixed: 3.1.57
pip install GitPython>=3.1.57
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show GitPython

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-3f7w-8rr8-f37f, inspect your installed packages using pip:

pip show GitPython

Analyze the terminal output to locate any version of GitPython in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.1.57), your application is affected.

Note: The pip show GitPython command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In GitPython, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

**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.check_unsafe_options()`, gated per method by an `allow_unsafe_options` 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.ls_remote()` (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.checkout_index(*args, **kwargs) # line 1331 ... proc = self.repo.git.checkout_index(args, **kwargs) # line 1349 ``` There is no `allow_unsafe_options` parameter and no `check_unsafe_options()` 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/target_dir/", a=True, f=True) ``` Observed (`poc/poc_checkout_index.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/poc_ci_overwrite.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 `authorized_keys`, `.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/poc_tag_F.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.from_tree()` | `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/poc_readtree.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.merge_base()` | `merge-base` | no path-taking option found | | `Repo._get_untracked_files()` | `status` | no path-taking option found | | `Remote.set_url()`, `Remote.create()`, `Remote.update()` | `remote` | URL handling already addressed by GHSA-94p4-4cq8-9g67 | ## Suggested remediation **Immediate:** add `allow_unsafe_options: bool = False` to both methods and gate `Git._option_candidates(args, kwargs)` against new lists — `unsafe_git_checkout_index_options = ["--prefix"]` (consider `--temp`) and `unsafe_git_tag_options = ["--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.ls_remote()`, `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._call_process()` — 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.

GHSA-3f7w-8rr8-f37f Affected GitPython Versions

1 component(s)
Fixed Versions:
3.1.57
Product / Package Ecosystem Affected Versions Fixed Version
GitPython
PyPI
< 3.1.57
3.1.57

References

Frequently Asked Questions

What is GHSA-3f7w-8rr8-f37f?
GHSA-3f7w-8rr8-f37f is a security vulnerability in GitPython involving Unguarded git option forwarding in IndexFile.checkout() and TagReference.create() enables arbitrary file overwrite and arbitrary file read under CWE-73: External Control of File Name or Path.
Which GitPython versions are affected?
Affected versions include releases prior to 3.1.57 across affected packages: GitPython.
What version fixes GHSA-3f7w-8rr8-f37f?
Upgrading to 3.1.57 (or the latest patched release for your respective branch) resolves the vulnerability.
Is GitPython affected?
Yes, GitPython is affected in releases prior to 3.1.57. Upgrade your dependency to 3.1.57 or later.
What is the CVSS score of GHSA-3f7w-8rr8-f37f?
GHSA-3f7w-8rr8-f37f has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H.
What CWE is associated with GHSA-3f7w-8rr8-f37f?
GHSA-3f7w-8rr8-f37f is classified under CWE-73: External Control of File Name or Path (The product allows user input to control or influence paths or file names that are used in filesystem operations.).
How can I check PyPI dependencies for GHSA-3f7w-8rr8-f37f?
Run the dependency tree command: 'pip show GitPython'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-3f7w-8rr8-f37f",
  "modified" : "2026-08-03T20:09:56Z",
  "published" : "2026-08-03T20:09:56Z",
  "aliases" : [ ],
  "summary" : "GitPython: Unguarded git option forwarding in IndexFile.checkout() and TagReference.create() enables arbitrary file overwrite and arbitrary file read",
  "details" : "**Target:** gitpython-developers/GitPython\n**Tested:** HEAD `07e80555` (2026-07-25), latest release 3.1.55, `git version 2.50.1`\n**Reported instances:** 2 exploitable, from a sweep of 14 unguarded call sites\n\n## Summary\n\nGitPython blocks dangerous git options through `Git.check_unsafe_options()`, gated per method by an `allow_unsafe_options` 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.\n\nA 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:\n\n| # | Call site | git option | Impact |\n|---|---|---|---|\n| 1 | `IndexFile.checkout()` → `git checkout-index` | `--prefix=<path>` | arbitrary file **overwrite** with repository-controlled content |\n| 2 | `TagReference.create()` → `git tag` | `-F <file>` / `--file=<file>` | arbitrary file **read**, returned in-band |\n\nThis is the same defect class already fixed in `Commit.count()` (GHSA-p538-c434-8v24), `Repo.archive()` and `Git.ls_remote()` (GHSA-956x-8gvw-wg5v). Both instances below are still present at HEAD.\n\n---\n\n## Instance 1 — `IndexFile.checkout()`: arbitrary file overwrite\n\n`git/index/base.py:1210` accepts `**kwargs` and forwards them with no guard:\n\n```python\ndef checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwargs):\n    ...\n    proc = self.repo.git.checkout_index(*args, **kwargs)   # line 1331\n    ...\n    proc = self.repo.git.checkout_index(args, **kwargs)    # line 1349\n```\n\nThere is no `allow_unsafe_options` parameter and no `check_unsafe_options()` call in the method.\n\n`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.\n\n### Reproduction\n\n```python\nfrom git import Repo\nRepo(\"/path/to/repo\").index.checkout(prefix=\"/tmp/target_dir/\", a=True, f=True)\n```\n\nObserved (`poc/poc_checkout_index.py`) — no exception raised, files land outside the repository:\n\n```\n[ALLOWED] no UnsafeOptionError raised\nfiles written outside the repo: ['f.txt']\n  f.txt: 'hi\\n'\n```\n\nOverwrite of a pre-existing file (`poc/poc_ci_overwrite.py`) — the victim file held `ORIGINAL-DO-NOT-CLOBBER\\n` before the call:\n\n```\n[ALLOWED] no exception\nvictim content now: 'hi\\n'\nOVERWRITTEN: True\n```\n\n### Why this rates High\n\nBoth halves of the write are attacker-influenced:\n\n- **Destination** — the `prefix` kwarg.\n- **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.\n\nCommit a file named `authorized_keys`, `.bashrc`, `config` or `post-checkout`, choose the matching prefix (`~/.ssh/`, `~/`, `.git/hooks/`), and the write becomes code execution as the service account.\n\nFor 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.\n\n---\n\n## Instance 2 — `TagReference.create()`: arbitrary file read\n\n`git/refs/tag.py:88` forwards `**kwargs` into `git tag` with no guard, and the signature advertises the passthrough:\n\n```python\ndef create(cls, repo, path, reference=\"HEAD\", logmsg=None, force=False, **kwargs):\n    \"\"\"...\n    :param kwargs:\n        Additional keyword arguments to be passed to :manpage:`git-tag(1)`.\n    \"\"\"\n```\n\n`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.\n\n### Reproduction\n\n```python\nfrom git import Repo\nfrom git.refs.tag import TagReference\n\nt = TagReference.create(Repo(\"/path/to/repo\"), \"x\", force=True, a=True, F=\"/etc/passwd\")\nprint(t.tag.message)\n```\n\nObserved (`poc/poc_tag_F.py`), reading a canary file outside the repository:\n\n```\n[ALLOWED] no UnsafeOptionError raised\n>>> tag message recovered from arbitrary path: 'TAG-READ-CANARY-98765\\nsecond-line-secret'\n```\n\nImpact 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.\n\n---\n\n## Sweep results — the other 12 sites\n\nReported so the fix can be scoped once rather than per report. `poc/sweep.py` reproduces this list.\n\n| Call site | git command | Assessment |\n|---|---|---|\n| `IndexFile.from_tree()` | `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/poc_readtree.py`) |\n| `IndexFile.remove()` | `rm` | `--pathspec-from-file` only reads a pathspec; no write or disclosure primitive found |\n| `IndexFile.move()` | `mv` | same |\n| `HEAD.reset()` | `reset` | same |\n| `HEAD.checkout()` | `checkout` | same |\n| `Head.delete()`, `RemoteReference.delete()` | `branch` | no path-taking option found |\n| `Repo.merge_base()` | `merge-base` | no path-taking option found |\n| `Repo._get_untracked_files()` | `status` | no path-taking option found |\n| `Remote.set_url()`, `Remote.create()`, `Remote.update()` | `remote` | URL handling already addressed by GHSA-94p4-4cq8-9g67 |\n\n## Suggested remediation\n\n**Immediate:** add `allow_unsafe_options: bool = False` to both methods and gate `Git._option_candidates(args, kwargs)` against new lists — `unsafe_git_checkout_index_options = [\"--prefix\"]` (consider `--temp`) and `unsafe_git_tag_options = [\"--file\", \"-F\"]` (consider `-s`, `-u`/`--local-user`, `--cleanup`) — matching the pattern used in `Repo.archive()` and `Commit.count()`.\n\n**Structural:** this defect has now been fixed four times in four places (`Repo.archive()`, `Git.ls_remote()`, `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._call_process()` — 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.\n\n## Disclosure\n\nReported privately via GitHub private vulnerability reporting.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "GitPython"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.1.57"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 3.1.56"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3f7w-8rr8-f37f"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/gitpython-developers/GitPython/pull/2193"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/gitpython-developers/GitPython/commit/3af0c2516c5e18c829da30338614688f6b69b49c"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/gitpython-developers/GitPython"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.57"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200", "CWE-22", "CWE-73" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-08-03T20:09:56Z",
    "nvd_published_at" : null
  }
}