GHSA-fx3j-rwgx-fr94: Infoleak

Published Oct 27, 2026
·
Updated

Summary Repo.clone()'s handling of separategitdir never disables Git.polishurl()'s environment-variable expansion, unlike every other caller of that helper that handles attacker-influenced data. Since separategitdir can be derived from an attacker-controlled .gitmodules submodule name, a completely ordinary clone + submoduleupdate(init=True, recursive=True) on a malicious repository expands an environment variable of the attacker's choosing and exposes its value both as a persistent on-disk directory name and, whenever DEBUG-level logging is enabled (a common CI/verbose-logging configuration), in a Popen(...) log line that is not redacted the way credential-bearing arguments are.

Root cause / Details git/repo/base.py's clone() has:

python sepdir = kwargs.get("separategitdir") if sepdir: kwargs["separategitdir"] = Git.polishurl(sepdir)

This is the only remaining Git.polishurl() call site handling attacker-influenced data that does not pass expandvars=False (there is one other unguarded call site, clonepath at line 1537, but its input is the caller's own local destination path, not attacker-controlled). The sibling fixes for GHSA-94p4-4cq8-9g67/GHSA-rwj8-pgh3-r573 added expandvars=False to cloneurl (5 lines below this one, in the same function), git/repo/base.py:1599, remote.py:814, and submodule/base.py:686 — this one did not receive it. (submodule/root.py also currently sets expandvars=False at a related call site, but that was added incidentally by a later, unrelated commit, not as part of these security fixes.) Git.polishurl()'s docstring itself says: "Disable this for values obtained from an untrusted source, such as remote URLs." Its non-Cygwin branch does plain os.path.expandvars() on the entire string, including embedded variables anywhere in an absolute path.

separategitdir reaches clone() from Submodule.clonerepo() (git/objects/submodule/base.py) as moduleabspath = osp.join(parentrepo.gitdir, "modules", name), where name is the raw .gitmodules submodule section name — fully attacker-controlled content of a cloned repository. validatedname() (added by the fix for GHSA-hmq2-w58f-27jc) only rejects names that are absolute, contain a drive letter, or contain .. path components; it does not reject $. needgitfilesubmodules() is true for git ≥ 1.7.5, i.e. the default path for virtually every real-world installation.

Reproduction (live, against real, unmodified GitPython code, no monkeypatching) Built a hostile repository with a plain, ordinary git command: git submodule add --name '$GPYSECRETMARKER' ../evilsubmodule sub. Set an environment variable in the "victim" process (standing in for a real CI secret such as AWSSECRETACCESSKEY): GPYSECRETMARKER=AKIATOTALLYFAKEBUTSECRETLOOKINGVALUE12345. Then ran the exact ordinary workflow the GHSA-hmq2-w58f-27jc advisory's own PoC also uses:

python repo = Repo.clonefrom(evilparenturl, victimclonedir) repo.submoduleupdate(init=True, recursive=True)

Observed result (ls .git/modules/):

.git/modules/AKIATOTALLYFAKEBUTSECRETLOOKINGVALUE12345/ (a full git repo: HEAD, config, objects/, refs/, hooks/)

The literal $GPYSECRETMARKER-named directory was never created — only the expanded one exists, proving expansion happened before the value reached the git clone --separate-git-dir=... subprocess argument. The persistent, expanded-name directory under .git/modules/ is itself the primary disclosure channel — it survives on disk for as long as the clone does. A second, independent channel: with the git.cmd/git.repo.base loggers at DEBUG level (a common CI/verbose-logging setting, not requiring any special flag), GitPython's own Popen(...) debug log line prints the fully expanded --separate-git-dir=<value> subprocess argument verbatim — removepasswordifpresent() only redacts URL passwords/Authorization headers, not path-style arguments like this one, so the expanded value is not scrubbed. (The call also fails downstream with an OSError/FileNotFoundError referencing the original, unexpanded name, since writegitfileandmoduleconfig() looks for a directory using the pre-expansion name — but that error message itself does not contain the expanded/secret value.)

Control: the same call with an ordinary (no $) submodule name succeeds normally and creates .git/modules/<name> as expected — isolating the gap to submodule names containing environment-variable syntax specifically.

Already-known check Checked all 35 published gitpython-developers/GitPython security advisories in full text. GHSA-hmq2-w58f-27jc covers the same name → .git/modules/<name> path but for path-traversal (../../..), not variable expansion. GHSA-59cr-6r3x-644w covers the sibling path field reaching unguarded os.makedirs(). GHSA-94p4-4cq8-9g67/GHSA-rwj8-pgh3-r573 cover expandvars, but only on URL arguments. git log -p -S "separategitdir" -- git/repo/base.py shows neither fix commit touches this line; it is unchanged since its 2016 introduction (commit 57d05379).

The closest topical match is GHSA-8mcc-hrx5-hvxc ("clonefrom()/clone() omit --separate-git-dir from unsafegitcloneoptions") — already fixed, and its own text discusses this same clone() handling of separategitdir. That fix only added --separate-git-dir to the unsafegitcloneoptions denylist, which gates the direct-kwarg vector; Submodule.clonerepo() explicitly sets allowunsafeoptions=True for the gitfile-submodules path, unconditionally bypassing that exact denylist (confirmed: the reproduction above succeeds against current main, which already has that fix). The bug this report describes is independent of that denylist — the value itself is expanded via os.path.expandvars() regardless of the denylist outcome — so the two are distinct, but flagging the relationship explicitly since a reviewer would otherwise reasonably ask about it.

No advisory describes environment-variable expansion via the internal separategitdir path.

Suggested fix Add expandvars=False to the Git.polishurl(sepdir) call in clone(), matching the four other call sites already fixed for the same class of issue.

Verification level Reproduced in a real environment: plain Linux, unmodified GitPython from current main, real git ≥ 1.7.5 behavior, an ordinary git submodule add + clone + submoduleupdate(init=True, recursive=True) workflow with no non-default configuration or monkeypatching required. Causation isolated via a control group (identical workflow, ordinary submodule name, no expansion).

AI Use Disclosure AI (Claude) was used as a tool under my direction for code review, code summarization, and script development. A human reviewed and verified the AI-generated results before submission.

Credit: please use "baeseungwon1010" (github.com/baeseungwon1010).

Affected Software

1 affected componentFixes available
pip/gitpython<=3.1.62
3.2.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/GitPython to a version that resolves this vulnerability.

    Fixed in 3.2.0
  2. Configuration

    Pass expand_vars=False to the Git.polish_url(sep_dir) call when handling separate_git_dir paths, preventing environment-variable expansion in attacker-controlled submodule names.

    GitPython Repo._clone() expand_vars = False

Event History

Oct 27, 2026
Advisory Published
via GitHub·03:26 PM
Data Sourced
via GitHub·03:26 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which workflows are exposed?

Workflows that clone an attacker-controlled repository and then run submodule_update(init=True, recursive=True) are exposed. This includes ordinary clone-and-recursive-submodule-update processing of a malicious repository.

2

What does an attacker need to control to trigger the issue?

The attacker needs to provide a malicious repository whose .gitmodules submodule name causes separate_git_dir to contain an environment-variable reference. No attacker authentication or prior privileges are indicated, but a user or automation job must process the repository.

3

When can the leaked value be observed?

The expanded value is persisted in an on-disk directory name. If DEBUG-level logging is enabled, it is also written to an unredacted Popen(...) log line; this is particularly relevant to CI or verbose-logging configurations.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203