GitPython version 3.1.50 blocks unsafe git clone options such as --upload-pack, -u, --config, and -c unless callers explicitly pass allow_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:
- Ecosystem: PyPI
- Package:
GitPython
- Confirmed affected version:
3.1.50
- Repository:
gitpython-developers/GitPython
- Current PyPI version during triage:
3.1.50
Relevant behavior:
Repo.unsafe_git_clone_options correctly lists --upload-pack, -u, --config, and -c as unsafe clone options.
Repo._clone() splits multi_options with shlex.split(" ".join(multi_options)) and then calls Git.check_unsafe_options(...).
_canonicalize_option_name("-u/path/to/helper") returns a string beginning with u..., not the canonical short option u, so it does not match the blocked -u entry.
- Git accepts the same joined short option as
--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 calls Repo.clone_from(..., multi_options=["-u<helper>"], allow_unsafe_options=False).
Observed sanitized output:
gitpython_version=3.1.50
git_version=git version 2.53.0.windows.1
tmp_dir=<tmp>
long_upload_pack_gate=BLOCKED:UnsafeOptionError
joined_short_upload_pack_gate=ALLOWED
clone_result=EXPECTED_EXCEPTION:GitCommandError
sentinel_exists=True
sentinel_text=GITPYTHON_UNSAFE_OPTION_BYPASS
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_options can bypass GitPython's default allow_unsafe_options=False protection and execute a local command via Git's --upload-pack / -u clone 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:
- OSV query for
PyPI/GitPython version 3.1.50 returned no vulnerabilities.
- The repository's public advisories include related unsafe Git option issues, including
GHSA-x2qx-6953-8485 / CVE-2026-42284 and GHSA-rpm5-65cw-6hj4 / CVE-2026-42215. Their public affected ranges are marked as fixed before 3.1.50.
GHSA-x2qx-6953-8485 describes validating multi_options before shlex.split(...). GitPython 3.1.50 now validates after splitting, but the joined short option -u<value> still bypasses because the validator canonicalizes it to u<value> rather than u.
GHSA-rpm5-65cw-6hj4 describes unsafe underscored kwargs such as upload_pack=.... The current PoC uses multi_options=["-u<helper>"] against 3.1.50 and does not depend on underscored kwargs.
- GitHub issue search for
upload-pack unsafe options found 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.
- GitHub issue search for
multi_options unsafe found PR #2130, which fixed splitting of multi_options before checking. The current issue remains after that split because -u<value> is treated as option name u<value>, not blocked short option u.
- GitHub issue searches for
u<upload-pack> unsafe and -cfoo returned no results.
Suggested remediation:
When checking unsafe Git options, parse joined short options that take values. For clone, -uVALUE and -cKEY=VALUE should be canonicalized to u and c respectively 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.
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.