chore(deps): update dependency gitpython to v3.1.58 [security] - autoclosed - #2587
Merged
Conversation
Signed-off-by: Renovate Bot <bot@renovateapp.com>
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.57→==3.1.58GitPython: Unguarded git read-tree option forwarding in IndexFile.from_tree/reset/merge_tree enables arbitrary file overwrite
GHSA-4gmw-gg2m-w46p
More information
Details
Summary
IndexFile.from_tree,IndexFile.reset(→ from_tree) andIndexFile.merge_treeappend caller-influenced treeish strings positionally togit read-treewith no unsafe-option guard, noallow_unsafe_optionsparameter, and no--separator.git read-tree --index-output=<file>writes the resulting index to an arbitrary path, and last-occurrence-wins lets an injected--index-outputoverride the method's internal temp path — clobbering an arbitrary file with a valid git-index blob. This is a distinct, never-guarded sink: commit3af0c251(GHSA-3f7w-8rr8-f37f) guarded onlycheckout_indexandtag;read_treewas left unprotected (it is among the acknowledged unguarded call sites in that advisory's sweep but was never reported or fixed).Root Cause
from_tree(index/base.py:388),reset(delegates to from_tree), andmerge_tree(index/base.py:291) callrepo.git.read_tree(*arg_list)with nocheck_unsafe_optionsand no--. The treeish is caller-influenced and positional.Impact
Arbitrary file overwrite / destruction at the privileges of the host process. Content is constrained to a git-index blob (not attacker-chosen, so not RCE), but the target path is fully attacker-controlled — corrupting/truncating configs or destroying files at attacker-chosen writable locations = I:H + A:H (per the skill's "overwrite-any-path = I:H" rule). Pure VALUE control (positional treeish). Default configuration.
Proof of Concept
Attack Chain
IndexFile.from_tree(repo, treeish)/reset(commit=…)/merge_tree(base=…, rhs=…)with attackertreeish="--index-output=/home/victim/.bashrc".allow_unsafe_optionsand never callcheck_unsafe_options.repo.git.read_tree(*arg_list)— no--. argv (from_tree, observed):['git','read-tree','--index-output=<tmp>','--index-output=/…/victim'](last-wins).Bypass Evidence
Independently reproduced (gate harness):
IndexFile.from_tree(repo,'--index-output=<victim>')→ victim overwritten; before=IMPORTANT ORIGINAL CONTENT, after startsDIRC\x00\x00\x00\x02…(destructive clobber, valid index blob).reset(commit=…)and bothmerge_treepositionals verified. Fix-commit read:3af0c251touched onlycheckout_index+tag;read_treeuntouched on HEAD.Affected Versions
GitPython <= 3.1.57(sinks present verbatim on the latest release tag).Suggested Fix
Add a
check_unsafe_optionsguard (with anallow_unsafe_optionsparameter) tofrom_tree/reset/merge_tree, and/or place a--separator before the positional treeish arguments; block--index-output(a path-taking option) on this sink.Reported by zx (Jace) — GitHub: @manus-use
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unguarded git option forwarding in Repo.init enables arbitrary command execution via --template clone hooks
GHSA-9rj7-rf2p-w77r
More information
Details
Summary
Repo.init()forwards**kwargsverbatim togit initwith no unsafe-option guard and noallow_unsafe_optionsparameter.git init --template=<dir>copies<dir>/hooks/*into the new repo's.git/hooks, so an attacker-controlledtemplatekwarg plants a hook that executes on the next git operation → arbitrary code execution.--templateis already recognized as unsafe for clone (it is onunsafe_git_clone_options, and GHSA-6p8h-3wgx-97gf covers the clone path), butRepo.initis a distinct method that never received a guard and needs an independent fix.Root Cause
Repo.init(path, mkdir, odbt, expand_vars, **kwargs)is a baregit.init(**kwargs)(git/repo/base.py:1435) with nocheck_unsafe_optionsand noallow_unsafe_options.Impact
Arbitrary code execution (hook fires on next git op) at the privileges of the host process. Two preconditions raise attack complexity (AC:H): the app must forward a
template=kwarg (KEY control) AND the attacker must stage an executable hook directory at a known path — the same profile GHSA-6p8h-3wgx-97gf accepted as HIGH for the clone path. Defaultallow_unsafe_optionsis irrelevant here becauseRepo.inithas no guard at all.Proof of Concept
Attack Chain
/evil/hooks/post-commit(executable) and gets the app to callRepo.init(path, template='/evil').Repo.init. Bypass proof: base.py:1435 is a baregit.init(**kwargs). argv (observed):['git','init','--template=/evil']./evil/hooks/post-commit→<repo>/.git/hooks/post-commit.Bypass Evidence
Independently reproduced (gate harness):
Repo.init(dst, template='<evil>')→ argv['git','init','--template=<evil>']unguarded; hook copied into.git/hooks/post-commit; aftergit committheINIT_ACEmarker was created.--separate-git-dir=<path>is a parallel arbitrary-redirect vector through the same unguarded sink (value control only).Affected Versions
GitPython <= 3.1.57(unguardedgit.init(**kwargs)present verbatim on the latest release tag).Suggested Fix
Add a
check_unsafe_optionsguard (with anallow_unsafe_optionsparameter) toRepo.init, consulting a denylist that includes--templateand--separate-git-dir(path-taking / hook-installing options).Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Arbitrary file read via --pathspec-from-file in IndexFile.remove() and Head.checkout()
GHSA-hh9p-6wh2-4mfc
More information
Details
Summary
IndexFile.remove()andHead.checkout()forward**kwargsintogit rmandgit checkoutwith no guard. Passing
--pathspec-from-file=<file>together with--pathspec-file-nulmakes Git treat the whole file as a single NUL-delimited pathspec, and the unmatched-pathspec
error quotes it verbatim. GitPython surfaces that through
GitCommandError.stderr, so theentire contents of a caller-chosen file are returned to the caller in band.
This is the same primitive as Instance 2 of
GHSA-3f7w-8rr8-f37f -
TagReference.create()with
-F, arbitrary file read returned in band - at two sites that advisory assessed andcleared.
Prior art, and why I am filing rather than commenting
GHSA-3f7w-8rr8-f37f's sweep table lists these four sites with the assessment
"
--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive found":IndexFile.remove()rm--pathspec-from-fileonly reads a pathspec; no write or disclosure primitive foundIndexFile.move()mvHEAD.reset()resetHEAD.checkout()checkoutThat assessment is very nearly right, and I think that is why it held: with
--pathspec-from-filealone, Git splits on newlines and the error quotes only the firstline, which reads as an uninteresting partial. Adding
--pathspec-file-nul- a sibling flagof the same option, and the documented way to handle paths containing newlines - makes the
whole file one pathspec.
Root cause
git/index/base.py:991-1043:git/refs/head.py:237-268:Neither has an
allow_unsafe_optionsparameter or acheck_unsafe_options()call.Proof of concept
Observed on published 3.1.57, against a canary file holding three marked lines:
The two precision controls are there so the result is about these sinks and not about the
canary being visible everywhere.
Scope correction to the table above
Of the four sites cleared with that sentence, two disclose and two do not:
IndexFile.remove()→git rmHead.checkout()→git checkoutHEAD.reset()→git resetgit resetdoes not error on unmatched pathspecsIndexFile.move()→git mvThe two negatives are mentioned because "the dismissal was wrong" would overstate it: the
dismissal was wrong for half of what it covered.
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 OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Arbitrary Git Repository Creation Outside the Working Tree via Unvalidated .gitmodules Submodule Name in GitPython
GHSA-hmq2-w58f-27jc
More information
Details
Summary
GitPython computes the on-disk location of a submodule's separate Git directory (
.git/modules/<name>) from the submodule's.gitmodulessection name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g.../../../../home/victim/.something) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition is that a victim clones the malicious repository with GitPython and runs submodule initialization (submodule_update(init=True)/sm.update(init=True)), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython's independent reimplementation never adopted an equivalent check.Details
src/GitPython/git/objects/submodule/util.pysm_name()strips thesubmodule "/"wrapper from a.gitmodules[submodule "..."]header and returns the result unchecked.Submodule.iter_items()insrc/GitPython/git/objects/submodule/base.pyreads this viasm_name(sms)and assigns it tosm._name; unlike the submodulepath,nameis never used for a tree lookup, so it is never implicitly validated.Submodule._module_abspath()then buildsosp.join(parent_repo.git_dir, "modules", name)-os.path.joindoes not normalize../sequences.Submodule._clone_repo()passes this value straight toos.makedirs()and togit clone --separate-git-dir=<module_abspath>, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for.PoC
FROM python:3.11-slim, withgitinstalled viaapt-get install -y git(Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git2.34.1, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container viapip install /src/GitPythonfrom this repository's own source, which the advisory states resolved to the officially releasedGitPython==3.1.57andgitdb==4.0.12.repo.submodules+sm.update(init=True), equivalent togit submodule update --init).$ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc . $ docker run --rm ghsa-gitpython-poc(Per the Dockerfile,
docker runexecutes/work/run_all.sh, which in turn runsbuild_attacker_repo.sh, thenpoc_gitpython.py, thenpoc_control_realgit.sh.)4. Full source of the PoC script (
GHSA/testing/poc_gitpython.py), verbatim:.gitmodulessection header from[submodule "legit_dir"]to[submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"](built bybuild_attacker_repo.sh, part of the harness inGHSA/testing/). The malicious part is the../../../../../../traversal sequence embedded in the submodule name (not the tree-validatedpath), which becomes the on-disk target for the submodule's separate git directory.gitCLI control run) rejects the submodule name with "ignoring suspicious submodule name" and refuses to create anything outside the repository. GitPython instead created the escape-target directory and a fully-initialized Git repository at/tmp/gitpython_poc_escaped_root/modules_dir, confirmed byescape_target exists after update: Trueand its listed contents.url.Impact
Path traversal (CWE-22) / external control of file path (CWE-73) leading to arbitrary directory and Git-repository creation outside the intended clone directory. Integrity impact is High (attacker chooses destination path and, via the submodule URL, much of the written content); Confidentiality impact is None (only creation was demonstrated); Availability impact is Low-Medium (disk-exhaustion potential). No authentication is required; the attacker only needs to control a repository the victim clones and initializes submodules for - a routine, often fully-automatic operation in CI pipelines, IDE integrations, and dependency-management tooling.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: git-config OPTION-name injection via =/#/whitespace bypasses name validator, enabling forged core.sshCommand/hooksPath (RCE)
GHSA-jm78-9fvv-mhgr
More information
Details
Summary
GitPython's config-name validator only neutralizes CR/LF/NUL for the
"option"label; it does not reject=,#,;,[,], or whitespace in an option name.write_sectionwrites the option name verbatim into the config file, so an option name such assshCommand = touch <cmd> #is written as\tsshCommand = touch <cmd> # = <value>, which git parses ascore.sshCommand = touch <cmd>(the trailing#comments out the intended value). This forges arbitrary config directives (core.sshCommand,core.hooksPath,alias.*) → RCE on the next git operation. This is a distinct field (option name, not section name) and distinct character class (=/#/space, not newline/bracket) from GHSA-3rp5-jjmw-4wv2 (section-name bracket injection) and GHSA-mv93-w799-cj2w / GHSA-v87r-6q3f-2j67 (newline injection).Root Cause
_assure_config_name_safe(name, label)(git/config.py:897) applies the bracket/quote state machine ONLY whenlabel == "section"; for the"option"label it falls through with just theUNSAFE_CONFIG_CHARS_RE = [\r\n\x00]regex.write_sectionthen writes the option name verbatim into"\t%s = %s\n"(config.py:702).Impact
Arbitrary git-config directive injection → remote code execution via
core.sshCommand(fires on any ssh git operation, no staged file needed) orcore.hooksPath(with a staged hook). Requires the embedding application to forward a caller-influenced OPTION NAME into the config writer (name-control model, the same name-control model accepted by the related published advisories GHSA-3rp5-jjmw-4wv2 and GHSA-mv93-w799-cj2w). Default configuration.Proof of Concept
Attack Chain
set_value("core", "sshCommand = touch /tmp/RCE #", "x")._assure_config_name_safe(option, "option")@ config.py. Guard: regex matches only[\r\n\x00]; bracket/quote state machine is gated onlabel=="section". Bypass proof:=,#,space pass → noValueError.write_sectionwrites"\tsshCommand = touch /tmp/RCE # = x\n"(config.py:702).core.sshCommand=touch /tmp/RCE→ arbitrary code execution on next git op.Bypass Evidence
Independently reproduced (gate harness):
set_value('core','sshCommand = touch <RCE> #','x')→ noValueError; file linesshCommand = touch <RCE> # = x;git config --get core.sshCommand→touch <RCE>(rc=0). Also verifiedcore.hooksPathvia bothGitConfigParserandrepo.config_writer(). Fix-commit read: bracket/quote checks are insideif label == "section"; the"option"label is not covered.Affected Versions
GitPython <= 3.1.57(validator present verbatim on the latest release tag).Suggested Fix
Apply the section-name safety checks (reject
=,#,;,[,], whitespace) to the"option"label as well, or validate the fully-rendered config line after substitution.Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
GHSA-wvpp-8hx9-p66j
More information
Details
Summary
The
check_unsafe_optionsguard can be bypassed on every guarded method (clone/clone_from, fetch/pull/push, ls_remote, iter_commits, blame, archive) by combining a single-character kwarg withsplit_single_char_options=False. The guard's candidate list omits the smuggled option, buttransform_kwargemits a JOINED-n<value>argv token that git parses as--upload-pack=<cmd>, yielding arbitrary command execution at the defaultallow_unsafe_options=False. This is an incomplete-fix bypass of commite8d0fbf7(the fix for GHSA-r9mr-m37c-5fr3), which only emits value-derived candidates whensplit_single_char_optionsis True.Root Cause
_option_candidatesderives value-token candidates only underif len(key)==1 and split_single_char_options:(cmd.py:1048, added bye8d0fbf7). Withsplit_single_char_options=False,_option_candidates([], {"n":"utouch <cmd>;git-upload-pack"})returns only['-n'](not on the denylist), so the guard passes. Buttransform_kwarg('n', value, split_single_char_options=False)emits the JOINED token-nutouch <cmd>;git-upload-pack(cmd.py:1631). git clusters value-less short flags then parses-u<cmd>=--upload-pack=<cmd>→ command execution. The hardened guard WOULD block the joined token if it saw it — the flaw is it never receives it.Impact
Arbitrary OS command execution as the host process (via
--upload-pack) at defaultallow_unsafe_options=False, affecting all guarded methods that forward kwargs. Precondition: the app forwards a user-controlled kwargs dict containingsplit_single_char_options=Falseplus a single-char key (same user-dict-forwarding model GHSA-r9mr-m37c-5fr3 accepts).Proof of Concept
Attack Chain
Repo.clone_from(url, path, **kwargs):{split_single_char_options: False, n: 'utouch /tmp/ACE;git-upload-pack'}.check_unsafe_options(_option_candidates([], kwargs), unsafe_git_clone_options). Guard: denylist includes--upload-pack/-u. Bypass proof:_option_candidatesyields only['-n'](value token skipped becausesplit=False); guard never sees-u.transform_kwargemits joined token (cmd.py:1631). argv (observed):['git','clone','-v','-nutouch /tmp/ACE;git-upload-pack','--','<src>','<dst>'].-n+-u<cmd>→ runs upload-pack command → ACE.Bypass Evidence
Independently reproduced (gate harness, default
allow_unsafe_options=False): thesplit=Falsepayload created the markerVH05_GATE_ACE(ACE); the clone returned normally (guard bypassed). Control:n='--upload-pack=…'(split default True) →UnsafeOptionError: --upload-pack is not allowed. Fix-commit read:e8d0fbf7extends candidates only underif len(key)==1 and split_single_char_options:— split=False skips value emission. Also confirmed the earlier clustering-parse fix (commit56806080) does not cover this because the guard only ever receives['-n'].Affected Versions
GitPython <= 3.1.57(code present verbatim on the latest release tag).Suggested Fix
Make
_option_candidatesemit value-derived candidates regardless ofsplit_single_char_options(i.e. also for the joined-n<value>form), OR runcheck_unsafe_optionsover the fully-transformed argv rather than the reconstructed name-only candidate list.Reported by zx (Jace) — GitHub: @manus-use
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Release Notes
gitpython-developers/GitPython (GitPython)
v3.1.58: Security and FixesCompare Source
What's Changed
New Contributors
Full Changelog: gitpython-developers/GitPython@3.1.57...3.1.58
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 these updates again.
This PR was generated by Mend Renovate. View the repository job log.