fix(deps): update dependency gitpython to v3.1.57 [security] - #336
Open
renovate[bot] wants to merge 1 commit into
Open
fix(deps): update dependency gitpython to v3.1.57 [security]#336renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
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.55→==3.1.57GitPython: Unguarded git option forwarding in IndexFile.checkout() and TagReference.create() enables arbitrary file overwrite and arbitrary file read
GHSA-3f7w-8rr8-f37f
More information
Details
Target: gitpython-developers/GitPython
Tested: HEAD
07e80555(2026-07-25), latest release 3.1.55,git version 2.50.1Reported instances: 2 exploitable, from a sweep of 14 unguarded call sites
Summary
GitPython blocks dangerous git options through
Git.check_unsafe_options(), gated per method by anallow_unsafe_optionsparameter. That guard is applied per call site, so any API that forwards**kwargsinto a git command without calling it passes caller-controlled options straight to git.A mechanical sweep of every method that forwards
**kwargsinto a.git.<command>(...)call found 14 sites with no guard. Two reach a git option that takes a filesystem path:IndexFile.checkout()→git checkout-index--prefix=<path>TagReference.create()→git tag-F <file>/--file=<file>This is the same defect class already fixed in
Commit.count()(GHSA-p538-c434-8v24),Repo.archive()andGit.ls_remote()(GHSA-956x-8gvw-wg5v). Both instances below are still present at HEAD.Instance 1 —
IndexFile.checkout(): arbitrary file overwritegit/index/base.py:1210accepts**kwargsand forwards them with no guard:There is no
allow_unsafe_optionsparameter and nocheck_unsafe_options()call in the method.git checkout-indexaccepts--prefix=<string>, prepended to every output path. It is not confined to the working tree, so an absolute prefix writes tracked file contents anywhere the process can write, and-foverwrites what is already there.Reproduction
Observed (
poc/poc_checkout_index.py) — no exception raised, files land outside the repository:Overwrite of a pre-existing file (
poc/poc_ci_overwrite.py) — the victim file heldORIGINAL-DO-NOT-CLOBBER\nbefore the call:Why this rates High
Both halves of the write are attacker-influenced:
prefixkwarg.Commit a file named
authorized_keys,.bashrc,configorpost-checkout, choose the matching prefix (~/.ssh/,~/,.git/hooks/), and the write becomes code execution as the service account.For comparison within this project: GHSA-fjr4-x663-mwxc (arbitrary file overwrite via
git diff --output) is rated High, and GHSA-p538-c434-8v24 (arbitrary file truncation viagit rev-list --output) is rated Medium.--prefixsupplies full content control, so it sits at or above the former.Instance 2 —
TagReference.create(): arbitrary file readgit/refs/tag.py:88forwards**kwargsintogit tagwith no guard, and the signature advertises the passthrough:git tagaccepts-F <file>/--file=<file>, which reads the tag message from an arbitrary path. The annotated tag object stores that content and GitPython returns it to the caller viaTagReference.tag.message, so the file contents come back in-band.Reproduction
Observed (
poc/poc_tag_F.py), reading a canary file outside the repository:Impact is a read at the privileges of the process. I am not claiming code execution for this instance. The signing options (
-s,-u/--local-user) do invoke gpg from the same unguarded kwargs, but I did not develop that into command execution and make no claim about it.Sweep results — the other 12 sites
Reported so the fix can be scoped once rather than per report.
poc/sweep.pyreproduces this list.IndexFile.from_tree()read-tree--index-output=<path>looked reachable but is neutralised: GitPython appends its own--index-outputafter the caller's kwargs and git honours the last occurrence. Verified — victim file unchanged (poc/poc_readtree.py)IndexFile.remove()rm--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive foundIndexFile.move()mvHEAD.reset()resetHEAD.checkout()checkoutHead.delete(),RemoteReference.delete()branchRepo.merge_base()merge-baseRepo._get_untracked_files()statusRemote.set_url(),Remote.create(),Remote.update()remoteSuggested remediation
Immediate: add
allow_unsafe_options: bool = Falseto both methods and gateGit._option_candidates(args, kwargs)against new lists —unsafe_git_checkout_index_options = ["--prefix"](consider--temp) andunsafe_git_tag_options = ["--file", "-F"](consider-s,-u/--local-user,--cleanup) — matching the pattern used inRepo.archive()andCommit.count().Structural: this defect has now been fixed four times in four places (
Repo.archive(),Git.ls_remote(),Commit.count(), and the two here), because the guard is opt-in per method: every new**kwargs-forwarding API starts unguarded and stays that way until someone reports it. Enforcing the check centrally inGit._call_process()— each git invocation consults a per-command unsafe-option table unless the caller opts out — would make new call sites safe by default rather than by review, and would close the remaining sites in the table above at the same time.Disclosure
Reported privately via GitHub private vulnerability reporting.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: Incomplete unsafe_git_archive_options denylist omits --add-file / --add-virtual-file, enabling arbitrary file read via Repo.archive()
GHSA-539m-9xh6-q6rr
More information
Details
Target: gitpython-developers/GitPython
Tested: HEAD
07e80555(2026-07-25), latest release 3.1.55,git version 2.50.1Summary
Repo.archive()does call the option guard, so this is not a missing-guard report. The guard is present and working; the denylist it consults is incomplete.The comment on
--outputstates the protected class in the project's own words: an option that lets the caller name a filesystem path is unsafe.--outputis blocked because it writes to a caller-chosen path.git archivealso accepts--add-file=<path>and--add-virtual-file=<path:content>(both present in current git; verified againstgit version 2.50.1).--add-filereads a caller-chosen path — including an absolute path outside the repository — and places the bytes into the archive the caller receives. Neither option is in the list, and no other layer references them:Net effect: the guard blocks arbitrary file write at this sink while permitting arbitrary file read at the same sink.
Reachability proof (verified at the sink)
poc/poc_addfile.pyat HEAD07e80555. The PoC creates its own out-of-tree canary, so it runs from a clean machine:The three blocked lines are the control: they prove the guard is active on this call path, so the fourth result is a gap in list membership rather than a guard that never ran.
Minimal reproduction:
The canary is untracked and lives outside the repository; its contents are recovered from the returned archive and asserted byte-for-byte against the on-disk file. The option is rendered by
transform_kwargsinto--add-file=<path>and reachesgit archiveunmodified.Direct precedent
GHSA-6p8h-3wgx-97gf(High, published 2026-07-22) is the same defect on the sibling list: "Incompleteunsafe_git_clone_optionsdenylist omits--template" — an option absent from one of these denylists, reachable under the same caller-controlled-options precondition, accepted and fixed by adding it.git logshows the archive list itself has already been extended reactively once, in701ce32f(fix: Guard unsafe git command options, GHSA-956x-8gvw-wg5v), and the--templateomission was then fixed separately inffcb5359.--add-virtual-fileis the same gap pointing the other way--add-virtual-file=<path:content>lets the caller inject attacker-chosen content under an attacker-chosen name into an archive that downstream consumers will reasonably treat as repository-derived.Suggested remediation
Repo.archive()has a small legitimate option surface (format,prefix,worktree_attributes,remote, compression level, plus paths). Accepting those and rejecting the rest means a future git release cannot add another path-taking option that silently reopens this.--add-fileand--add-virtual-file, and make the membership rule "the option takes a filesystem path or URL" rather than "the option executes a command". The existing comment on--outputalready implies that rule; applying it consistently is what closes the class instead of this instance.Scope limits
Repo.archive(). That is the identical precondition to--output,--execand--template, all of which this project has treated as reportable.Disclosure
Reported privately via GitHub private vulnerability reporting. Happy to test a candidate patch against the PoC. No public disclosure until you have shipped a fix and are ready.
Addendum (2026-07-25) — related observation on the same membership question, filed here rather than separately
While auditing the archive denylist, the same class of gap was identified in unsafe_git_clone_options. A second advisory is not being requested, as the issue is lower severity and should inform the fix for the issue above rather than require separate triage. Recording it here to provide the complete picture in one place.
Repo._clone()treats a URL's protocol as a security boundary and appliescheck_unsafe_protocols()to exactly one input:git cloneaccepts a second URL via--bundle-uri=<uri>, which git dereferences before the main transport runs. That option is absent fromunsafe_git_clone_options, so the option guard passes it, andcheck_unsafe_protocols()never inspects it. A caller-influenced value therefore drives an outbound request from the host:Confirmed against a local listener — the request leaves the process:
file:///pathis likewise accepted without error. Note this is not a tokenisation bypass:multi_optionsisshlex.splitbefore the check (perc9a26789/ GHSA-x2qx-6953-8485), so the fully-split--bundle-uri=...token is checked and legitimately passes because the option is not on the list.Why it belongs with this report: both are the membership question rather than the matching logic — is the set of blocked options complete, and does the protocol guard inspect every URL git will dereference? The structural remediation proposed above covers both if extended slightly: prefer an allowlist per command, and route every URL-bearing option through
check_unsafe_protocols(), not only the positional URL. Adding--bundle-uritounsafe_git_clone_optionswould be the minimal fix.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: Arbitrary file truncation via git rev-list --output argument injection in unguarded Commit.count
GHSA-p538-c434-8v24
More information
Details
Summary
Commit.count()forwards**kwargsintorev_listwith nocheck_unsafe_optionsguard (the guard exists only in the siblingiter_items, commit.py:341).git rev-list --output=<path>opens and truncates the target file to 0 bytes before revision parsing, socount(output='/victim')destroys/blanks an arbitrary file.Root Cause
commit.py:290-291callsself.repo.git.rev_list(self.hexsha, **kwargs)with nocheck_unsafe_optionsand noallow_unsafe_optionsparameter. The siblingiter_items(commit.py:341) is guarded;countis not. This is a distinct, uncovered sink — GHSA-956x-8gvw-wg5v fixediter_commits/blame, notcount.Impact
Destroy/blank an arbitrary file at process privilege (integrity/availability). Reachability is key-control only (
countusesself.hexsha, not a user ref), and the write is a 0-byte truncation (no content control), so MEDIUM.Proof of Concept
Attack Chain
commit.count(output='/victim'). Guard: none. Bypass proof:iter_commits(output=)raises UnsafeOptionError;count(output=)does not — verified side-by-side.git rev-list <sha> --output=/victim-> file truncated to 0 bytes. Impact: destroy/blank arbitrary file.Bypass Evidence
Live-verified on HEAD (tag 3.1.53):
count(output=<victim>)truncated a pre-existing file to 0 bytes; guardediter_commits(output=)raised UnsafeOptionError. Same CNA-accepted "app forwards user options dict" model as GHSA-956x-8gvw-wg5v'sarchive(**kwargs). Uncovered sink, not a duplicate.Affected Versions
<= 3.1.53Suggested Fix
Add
check_unsafe_optionstoCommit.count(mirroringiter_items).Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
gitpython-developers/GitPython (gitpython)
v3.1.57: - Security and FixesCompare Source
What's Changed
New Contributors
Full Changelog: gitpython-developers/GitPython@3.1.56...3.1.57
v3.1.56: - SECURITYCompare Source
What's Changed
Full Changelog: gitpython-developers/GitPython@3.1.55...3.1.56
Configuration
📅 Schedule: (in timezone Europe/Berlin)
🚦 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.