chore(deps): update dependency gitpython to v3.1.62 [security] - #5722
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
Pull request dashboard statusWaiting on reviewers · refreshed 2026-10-04 00:26 UTC Review the latest changes. Status above doesn't look right?
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
==3.1.59→==3.1.62GitPython: Repository content can impersonate the git directory, leading to arbitrary code execution
CVE-2026-87817 / GHSA-239g-whfq-7xj9
More information
Details
Summary
Repo.__init__decides which directory is the git directory by testing candidate paths in an order thatconsiders the real
.gitlast. Two earlier tests can be satisfied by ordinary tracked files. Gitreserves only the literal name
.git, soHEAD,objects/,refs/,config,gitdir,commondirand
hooks/at a repository root are all legal tracked content.Consequently, after a victim opens or clones an attacker's repository, GitPython resolves
git_dirtothe working-tree root while real git correctly resolves
<root>/.git. Everything GitPython thentreats as "inside the git directory" is attacker-authored content — including
hooks/, which itexecutes.
CVE-2026-87817
Affected code (3.1.59)
The discovery loop in
git/repo/base.pytests, in order:git/repo/base.py:299—isfile(curpath/gitdir)andisfile(curpath/commondir)andisfile(curpath/HEAD)git/repo/base.py:320—is_git_dir(curpath)git/repo/base.py:341—dotgit = osp.join(curpath, ".git")← the real git dir, considered lastis_git_dir(git/repo/fun.py:60) requires only thatobjects/andrefs/are directories and thatHEADis a file;HEAD's contents are never parsed. The hook path is resolved fromindex.repo.git_dir(git/index/fun.py:73), i.e. the mis-resolved directory.Proof of concept
Requires only
pip install GitPython==3.1.59. Full script attached aspoc1_rce.py; it runs entirelyin a temp directory and the payload only writes a marker file.
Attacker repository — four ordinary tracked files at the root:
gitdir.git\ncommondir.git\nHEADref: refs/heads/master\nhooks/pre-commit#!/bin/sh+ payloadVictim — two ordinary calls:
Observed on the PyPI release 3.1.59 (Linux and Windows):
The
pre-commithook runs atgit/index/base.py:1201, beforewrite_tree(), so it fires even thoughthe commit later fails.
Impact
A service that opens or clones an untrusted repository with GitPython — a CI runner building a fork
pull request, a code-scanning/SBOM service, a mirror, a dependency bot, an AI code-review/agent tool —
can be made to:
<root>/hooks/pre-commitwhen the victim callsindex.commit().<root>/configbecomes the repository config andis parsed with
merge_includes=True(git/repo/base.py:765), so[include] path = ~/.aws/credentialsdiscloses the file. (
Repo._config_readerstill defaultsmerge_includes=True, so the hardeningadded in 3.1.59 for
.gitmodules/GHSA-7833 does not cover this path.)commondir.Delivery is silent:
git cloneexits 0,git fsck(including--strict, and withtransfer.fsckObjects/fetch.fsckObjects=true) reports nothing, and the clone passes every read-onlyprobe (
head.commit,branches,is_dirty(),untracked_files,iter_commits) because a trackedcommondirof.gitpinscommon_dirto the real.git.Threat model / preconditions
fork PR, mirrored dependency).
index.commit(). For thefile-read impact, opening the repo and reading config is enough.
GIT_WORK_TREEis not a mitigation —git_diris assigned and the discovery loop breaks beforethe environment is consulted.
Remediation
curpath/.gitbefore thegitdir/commondir/HEADtriple and beforeis_git_dir(curpath).hook_path/_commit_hook_path/_get_validated_reflog_paththroughrepo.common_dir.commondir/gitdircontents before joining (reuseSymbolicReference._get_validated_path).HEADinis_git_dir(requireref: refs/...or a 40-hex sha).merge_includes=FalseinRepo._config_reader(git/repo/base.py:765).Note for the maintainers
Commit
406b98e1(2026-05-31, "respect core.hooksPath for commit hooks") resolved the hook path viagit rev-parse --git-path, which is immune because git rediscovers the true.git. Commit9bc287a2(2026-07-20) reverted it to avoid an unconditional
rev-parsedependency — reintroducing the exec step.Measured at each commit with the healthy-looking layout:
406b98e1→ hook did not fire;9bc287a2…3.1.59→ hook fired.Scope note
Only the repository-root case is reported: where a real
.gitexists, git prefers it, and GitPythondoes not. A fake git dir in a subdirectory fools real git too, so that variant is out of scope as a
general ecosystem hazard rather than a GitPython defect.
###POC Files :
poc1_rce.py
poc2_file_read.py
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: --no-index bypasses diff unsafe-option protections and enables a blind local-file content oracle
GHSA-whh4-5q6c-9v3x
More information
Details
Summary
GitPython 3.1.59 blocks a previously available local-file read path through unsafe git diff options such as -O/--orderfile.
However, the high-level diff API still permits --no-index with the default allow_unsafe_options=False.
--no-index changes the semantics of the paths arguments: instead of repository-relative pathspecs, Git interprets them as arbitrary filesystem paths.
When combined with the still-allowed -I/--ignore-matching-lines option, this creates a content-dependent Boolean oracle over a caller-selected local file.
This was reproduced against the published GitPython 3.1.59 wheel.
The original unsafe-option path is blocked in 3.1.59, while this alternate path remains reachable without setting allow_unsafe_options=True.
Details
Confirmed API surface:
repo.index.diff(
None,
no_index=True,
I=pattern,
paths=[baseline_path, target_path],
create_patch=True,
)
The relevant behavior is:
An application that forwards attacker-influenced diff options and paths and exposes the success/error distinction can therefore be queried repeatedly to recover a guessable local single-line secret.
The issue was confirmed with the default allow_unsafe_options=False.
The behavior does not require the caller to explicitly opt into GitPython's unsafe-option mode.
Verified intended security boundary:
This appears to be an alternate route to the same local-file confidentiality property that the 3.1.59 diff option hardening is intended to protect.
PoC
A minimal reproducer, controlled extraction demonstrator, proof matrix, and proposed remediation are included in the attached package.
gitpython-3159-maintainer-evidence.zip
The minimal reproducer creates only temporary researcher-controlled files and demonstrates the following predicate:
correct prefix -> normal GitPython return
incorrect prefix -> GitCommandError(status=1)
In the controlled extraction test, I generated three independent random single-line values and recovered all three exactly through repeated calls to the GitPython high-level API.
Result: 3/3 recovered.
The extraction harness also installs a Python audit hook that rejects direct Python open() access to the target file during the oracle phase. The content-dependent read is therefore performed by the child git process invoked through GitPython rather than by the reproduction script directly.
Controls were also tested:
Suggested remediation is to classify --no-index as unsafe for the high-level diff API unless the caller explicitly sets allow_unsafe_options=True.
Impact
Potential impact is disclosure of local files readable by the process running GitPython.
Exploitation requires an embedding application to allow an attacker to influence:
The demonstrated attack is a blind content oracle rather than a one-request in-band file read. It is particularly applicable to short or structured single-line secrets where the target path and approximate value format are known or guessable.
Confirmed affected release: GitPython 3.1.59.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: Denial of Service via catastrophic backtracking (ReDoS) in Actor.name_email_regex — commit author/committer field parsing
CVE-2026-87819 / GHSA-g5vv-9gxw-82hx
More information
Details
Summary
GitPython's
Actor.name_email_regexregular expression (git/util.py, line 863)is vulnerable to catastrophic backtracking (ReDoS — Regular Expression Denial of
Service). When GitPython parses the
authororcommitterheader of a git commitobject that contains a long string with an unterminated
<(no matching>), thePython 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
.authoror.committerfor over two minutes per invocation, enabling denialof 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:This regex is evaluated inside
Actor._from_string()(line 909) every timeGitPython resolves a commit's
.authoror.committerproperty.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_lineis decoded directly from the raw bytes of the git commit object withno length limit, character restriction, or timeout applied at any point before
reaching the regex engine. The same chain is triggered by:
commit.authorcommit.committerrepo.iter_commits()repo.blame()Why this pattern backtracks catastrophically:
The pattern
(.*) <(.*?)>contains an unbounded greedy group(.*)followed bya literal space and
<. When the input is a long string that contains<but noclosing
>, the regex engine must try every possible split position for the greedygroup — 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):
.authorEach 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>confirmsit is a valid
committype 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 authorline. Delivery paths that bypass those checks include:
receive.fsckObjects = false(common in self-hosted deployments).gitdirectory shipped as a tarball, backup, or zip archivePoC
Environment used for testing:
Script 1 — craft the malicious repository (
craft_malicious_repo.py):Script 2 — trigger the vulnerability (
trigger_redos.py):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):
The
python:3.8-bookwormbase image matches the one used in GitPython's ownfuzzing/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:
that clone and inspect third-party pull requests — one malicious commit in a PR
can stall every worker that processes it.
— a single crafted push blocks every page render or API response that touches that
commit's metadata.
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
.authoraccess. When
iter_commits()orblameare used, every commit in a historytraversal 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 namegroup, removing the ambiguity that forces O(n²) backtracking:
Because the name group can no longer itself contain a
<character, the enginehas exactly one candidate position to try when a closing
>is absent — and failsin O(n) time instead of O(n²). Legitimate actor strings (
Name <email>) nevercontain
<or>in either field, so this change produces identical results forall 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.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython submodule update path traversal can write outside the repository
GHSA-59cr-6r3x-644w
More information
Details
Affected:
GitPython3.1.61 (latest release) andmain—git/objects/submodule/base.py.git diff 3.1.61 origin/main -- git/objects/submodule/is empty, so both are identical here.The gap
The fix for
GHSA-hmq2-w58f-27jcaddedSubmodule._validated_name()and wired it intoupdate()and five siblings, closing the.gitmodulesname →.git/modules/<name>traversal. The other attacker-controlled.gitmodulesfield,path, is read raw:and GitPython's own containment guard is applied in only two of the places that consume it:
update()validates only the name and then uses the path-derived absolute location directly:So
path = ../../../tmp/escapedin an attacker-authored.gitmodulesselects the directory that gets created and, on the clone path, populated from the submodule URL. The same absolute location is whatforce_removehands toshutil.rmtree.The asymmetry is the argument: this is not a missing concept — the project wrote
_to_relative_path()precisely for this, andadd()/move()use it.update()does not.Honest limits (please read before rating)
Repo.clone_from(...)→repo.submodules→sm.update(init=True)re-derivespathfrom a canonical tree lookup, and real git refuses to check out a tree containing a..component, so an evil.gitmodulesnever lands in the working tree in the first place. A reachable trigger therefore requires the victim's code to name a non-HEAD commit (a historical-commit API such assubmodule_update(previous_commit=...)).update(), and the unguardedabspath→os.makedirs()flow at 3.1.61 ==main.Suggested fix
Apply the guard the project already has, wherever the path is consumed:
Better still, validate at the boundary: reject a
.gitmodulesentry whosepathis absolute or contains a..component when the section is first read in_set_cache_()/iter_items(), so no consumer can be added later without the check. A regression test withpath = ../escapedalongside the existingnametest would pin both fields.Prior art checked
GHSA-hmq2-w58f-27jc(this is a residual of its fix, in the sibling field, not a re-report) plus the repository's 30 published advisories — none mentions thepathfield or_to_relative_path. Searched issues and PRs for_to_relative_path,gitmodules pathandsubmodule traversal: no report of this.Credit
kta1kri.
Appendix —
EVIDENCE_gitpython_path_unguarded_20260901.txt(inlined; advisories accept no attachments)Severity
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:H/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
gitpython-developers/GitPython (GitPython)
v3.1.62Compare Source
What's Changed
New Contributors
Full Changelog: gitpython-developers/GitPython@3.1.61...3.1.62
v3.1.61Compare Source
Fix accidental removal of exploitable regex in
Actorby bringing it back, and deprecating it.What's Changed
New Contributors
Full Changelog: gitpython-developers/GitPython@3.1.60...3.1.61
v3.1.60: SecurityCompare Source
What's Changed
Full Changelog: gitpython-developers/GitPython@3.1.59...3.1.60
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.