high · 7.5CVE-2026-78677Sep 8, 2026

CVE-2026-78677: GitPython clone_from() Path Traversal via --separate-git-dir

Shubham Kandhare
Security Engagement Manager, SecureLayer7

GitPython's clone_from() and clone() fail to block the --separate-git-dir option, letting an attacker redirect the entire .git metadata directory to any path on disk outside the intended clone…

PackageGitPython
Ecosystempip
Affected<= 3.1.58
Fixed in3.1.59
CVE-2026-78677: GitPython clone_from() Path Traversal via --separate-git-dir

The problem

GitPython maintains a denylist, Repo.unsafe_git_clone_options, to block dangerous git clone flags when allow_unsafe_options is left at its default False. In versions up to and including 3.1.58, --separate-git-dir is absent from that list.

git clone --separate-git-dir=<path> writes the full repository metadata tree (config, HEAD, hooks/, objects/, refs/, etc.) to an arbitrary caller-controlled path, leaving only a gitlink text file at the intended destination. Any application that forwards an attacker-influenced separate_git_dir keyword argument into Repo.clone_from() or Repo.clone() gets no protection, even with the default safe settings.

Proof of concept

A working proof-of-concept for CVE-2026-78677 in GitPython, with the exact payload below.

python
import git

# Attacker controls `target_gitdir` -- any writable path outside the sandbox.
git.Repo.clone_from(
    "https://github.com/example/repo.git",
    "/safe/sandbox/dest",
    separate_git_dir="/tmp/attacker_controlled/redirected.git"
    # allow_unsafe_options is False by default -- no UnsafeOptionError is raised
)
# Result: full git directory (hooks/, config, objects/, refs/, ...) created at
# /tmp/attacker_controlled/redirected.git, outside the intended destination.
# /safe/sandbox/dest/.git becomes a gitlink: "gitdir: /tmp/attacker_controlled/redirected.git"

The root cause is a parity gap between two sibling denylists in git/repo/base.py. unsafe_git_init_options (lines 145-151) correctly lists --separate-git-dir with the comment 'Redirects the repository metadata to a caller-controlled path'. unsafe_git_clone_options (lines 153-165), which guards the same option on git clone, simply omits it.

The clone_from() docstring even names --separate-git-dir as one of the options allow_unsafe_options is supposed to gate, confirming the maintainers intended it to be blocked. Git.check_unsafe_options() checks only option names against the denylist, so the missing entry means the option passes through unchallenged and reaches the real git subprocess as --separate-git-dir=<attacker path>.

This is the same 'denylist omits a dangerous sibling option' pattern behind several prior GitPython advisories (e.g., GHSA-6p8h-3wgx-97gf for --template on clone). The fix is one line: adding "--separate-git-dir" to unsafe_git_clone_options, matching the already-correct unsafe_git_init_options.

The fix

Upgrade to GitPython 3.1.59 or later. The patch (commit b68afff45af0f49e79a3e2d2162018986b37ad5d, PR #2210) adds "--separate-git-dir" to Repo.unsafe_git_clone_options in git/repo/base.py, making check_unsafe_options() raise UnsafeOptionError if the option is passed without allow_unsafe_options=True.

If an immediate upgrade is not possible, audit all call sites of Repo.clone_from() and Repo.clone() and reject any caller-supplied value for the separate_git_dir keyword argument at the application layer.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research