CVE-2026-114145
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- Required
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
CVE-2026-114145- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Weakness
- CWE-696
- Updated
- 2026-11-02
EUVDEUVD-2026-107557
7.5
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
EUVD-2026-107557- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-83vg-56qc-22m7
7.5
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
Summary
create_submodule(name, path, url) clones a submodule into the repo's own .git
directory before it rejects the path. A path of ".git/hooks" is accepted by the
pre-clone check, so the submodule is cloned straight into the live hooks directory,
and a file named post-checkout in the submodule becomes an executable
.git/hooks/post-checkout. On 3.2.0+ the later index check does raise ValueError,
but only after the clone already wrote to disk, and it does not remove the planted
hook; on 3.1.x there is no such check and the call succeeds silently. Either way the
hook runs on the next git operation (e.g. checkout), giving code execution.
Details
The checkout path is validated too late, after the clone has already mutated disk. This breaks the "validate before mutation" invariant that commit 43a43cd8 ("Validate submodule checkout and metadata paths before mutation") set out to enforce.
Submodule.add() calls the permissive path check before cloning, and the strict check only reaches the ".git" alias at index time, after _clone_repo has written to disk:
git/objects/submodule/base.py (Submodule.add) 599: path = cls._to_relative_path(repo, path) # accepts ".git/hooks" 673: mrepo = cls._clone_repo(repo, url, path, name, ...) # clones into <worktree>/.git/hooks 714: index.add([sm], write=True) # strict check only triggers here
git/index/base.py (IndexFile._preprocess_add_items) 732: _validate_repo_path(entry.path) # rejects ".git", too late
_to_relative_path only blocks ../absolute/out-of-tree paths; .git aliases are
rejected only by _validate_repo_path, which was added in 3.2.0 (commit a75aedf3) and
runs at the index boundary. The ValueError it raises at line 732 does not roll back
what _clone_repo already wrote.
Version-dependent behavior:
- 3.2.0 .. tip bda18e08: _validate_repo_path raises ValueError at index.add, but the hook is already on disk and is not removed (incorrect order of validation vs. clone).
- 3.1.x (e.g. 3.1.59): _validate_repo_path does not exist and _to_relative_path does
not reject
.git; create_submodule(path=".git/hooks", ...) returns with no error at all and the hook is planted silently.
Proof of concept
Setup:
evilsub/ : a git repo whose ROOT contains an executable file named post-checkout (the payload)
victim/ : a git repo with at least 1 commit and a second branch, whose .git/hooks has NO
sample hooks (the state of a minimal/CI checkout, or git init with an empty
template dir, or GIT_TEMPLATE_DIR/init.templateDir empty)
import git, os.path as osp try: git.Repo("victim").create_submodule(name="x", path=".git/hooks", url=osp.abspath("evilsub")) except ValueError: pass # 3.2.0+: ".git" is rejected, but only AFTER the clone, so the plant already happened. # 3.1.x: no exception is raised at all; the call succeeds and still plants the hook.
victim/.git/hooks/post-checkout now exists, mode 0755, attacker-controlled, on every
affected version. Any later checkout runs it:
git -C victim checkout <other-branch> -> the hook runs -> code execution
Verified live:
- tip bda18e08 (3.2.0 + 3 commits): create_submodule raised ValueError from
git/index/base.py:732, yet .git/hooks/post-checkout was present and executable, and
git checkoutexecuted it. - 3.1.59: create_submodule returned with no error; .git/hooks/post-checkout was planted
and executed by
git checkout.
Impact
Arbitrary code execution. A library/app that creates a submodule from an
attacker-influenced path ends up with an attacker-controlled executable in
.git/hooks, which git runs on ordinary later operations. This affects any service
that derives a submodule checkout path from untrusted data, e.g. one that ingests a
submodule spec (name/path/url) from a user-supplied manifest or API request and feeds
path into create_submodule. On 3.2.0+ the library's…
- Record
GHSA-83vg-56qc-22m7- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Weakness
- CWE-696
- Updated
- 2026-11-02
NVD
7.5
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
CVE-2026-114145- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Weakness
- CWE-696
- Updated
- 2026-11-02
OSV
7.5
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
CVE-2026-114145- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Weakness
- CWE-696
- Updated
- 2026-11-03
OSVDEBIAN-CVE-2026-114145
7.5
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
DEBIAN-CVE-2026-114145- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
Show 4 moreShow less
OSVGHSA-83vg-56qc-22m7
7.5
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
Summary
create_submodule(name, path, url) clones a submodule into the repo's own .git
directory before it rejects the path. A path of ".git/hooks" is accepted by the
pre-clone check, so the submodule is cloned straight into the live hooks directory,
and a file named post-checkout in the submodule becomes an executable
.git/hooks/post-checkout. On 3.2.0+ the later index check does raise ValueError,
but only after the clone already wrote to disk, and it does not remove the planted
hook; on 3.1.x there is no such check and the call succeeds silently. Either way the
hook runs on the next git operation (e.g. checkout), giving code execution.
Details
The checkout path is validated too late, after the clone has already mutated disk. This breaks the "validate before mutation" invariant that commit 43a43cd8 ("Validate submodule checkout and metadata paths before mutation") set out to enforce.
Submodule.add() calls the permissive path check before cloning, and the strict check only reaches the ".git" alias at index time, after _clone_repo has written to disk:
git/objects/submodule/base.py (Submodule.add) 599: path = cls._to_relative_path(repo, path) # accepts ".git/hooks" 673: mrepo = cls._clone_repo(repo, url, path, name, ...) # clones into <worktree>/.git/hooks 714: index.add([sm], write=True) # strict check only triggers here
git/index/base.py (IndexFile._preprocess_add_items) 732: _validate_repo_path(entry.path) # rejects ".git", too late
_to_relative_path only blocks ../absolute/out-of-tree paths; .git aliases are
rejected only by _validate_repo_path, which was added in 3.2.0 (commit a75aedf3) and
runs at the index boundary. The ValueError it raises at line 732 does not roll back
what _clone_repo already wrote.
Version-dependent behavior:
- 3.2.0 .. tip bda18e08: _validate_repo_path raises ValueError at index.add, but the hook is already on disk and is not removed (incorrect order of validation vs. clone).
- 3.1.x (e.g. 3.1.59): _validate_repo_path does not exist and _to_relative_path does
not reject
.git; create_submodule(path=".git/hooks", ...) returns with no error at all and the hook is planted silently.
Proof of concept
Setup:
evilsub/ : a git repo whose ROOT contains an executable file named post-checkout (the payload)
victim/ : a git repo with at least 1 commit and a second branch, whose .git/hooks has NO
sample hooks (the state of a minimal/CI checkout, or git init with an empty
template dir, or GIT_TEMPLATE_DIR/init.templateDir empty)
import git, os.path as osp try: git.Repo("victim").create_submodule(name="x", path=".git/hooks", url=osp.abspath("evilsub")) except ValueError: pass # 3.2.0+: ".git" is rejected, but only AFTER the clone, so the plant already happened. # 3.1.x: no exception is raised at all; the call succeeds and still plants the hook.
victim/.git/hooks/post-checkout now exists, mode 0755, attacker-controlled, on every
affected version. Any later checkout runs it:
git -C victim checkout <other-branch> -> the hook runs -> code execution
Verified live:
- tip bda18e08 (3.2.0 + 3 commits): create_submodule raised ValueError from
git/index/base.py:732, yet .git/hooks/post-checkout was present and executable, and
git checkoutexecuted it. - 3.1.59: create_submodule returned with no error; .git/hooks/post-checkout was planted
and executed by
git checkout.
Impact
Arbitrary code execution. A library/app that creates a submodule from an
attacker-influenced path ends up with an attacker-controlled executable in
.git/hooks, which git runs on ordinary later operations. This affects any service
that derives a submodule checkout path from untrusted data, e.g. one that ingests a
submodule spec (name/path/url) from a user-supplied manifest or API request and feeds
path into create_submodule. On 3.2.0+ the library's…
- Record
GHSA-83vg-56qc-22m7- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Weakness
- CWE-696
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-114145
7.5
GitPython is a python library used to interact with Git repositories. Prior to 3.2.1, create_submodule() in git/objects/submodule/base.py accepts an attacker-influenced submodule path such as .git/hooks and invokes _clone_repo() before safely rejecting the destination. In 3.1.x, _validate_repo_path() does not exist and the call succeeds silently, while in 3.2.0 and later affected revisions the check in git/index/base.py raises ValueError only after the clone has written to disk and does not remove the planted file. An attacker-controlled submodule can therefore place an executable post-checkout hook that a later Git operation executes. Exploitation requires an application to accept an untrusted submodule path and a clone destination such as .git/hooks that is empty enough for Git to clone into it, which commonly means that default sample hooks are absent. This issue is fixed in version 3.2.1.
- Record
UBUNTU-CVE-2026-114145- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
OSVCLSA-2026-1792741260 No score
TuxCare security update for gitpython (1 CVE)
TuxCare rebuilt gitpython to address 1 CVE. Fixed in 3.1.31.post17+tuxcare.
- Record
CLSA-2026-1792741260- Updated
- 2026-11-02
OSVopenSUSE-SU-2026:12391-1 No score
python313-GitPython-3.2.1-1.1 on GA media
These are all security issues fixed in the python313-GitPython-3.2.1-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:12391-1- Updated
- 2026-11-03
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
gitpythonPyPI |
through 3.2.0 | 3.2.1 |
gitpythonTuxCare:PyPI |
3.1.31 up to 3.1.31.post17+tuxcare, plus listed versions 3.1.31.post1+tuxcare3.1.31.post10+tuxcare3.1.31.post11+tuxcare3.1.31.post12+tuxcare3.1.31.post13+tuxcare Show 11 moreShow less3.1.31.post14+tuxcare3.1.31.post15+tuxcare3.1.31.post16+tuxcare3.1.31.post2+tuxcare3.1.31.post3+tuxcare3.1.31.post4+tuxcare3.1.31.post5+tuxcare3.1.31.post6+tuxcare3.1.31.post7+tuxcare3.1.31.post8+tuxcare3.1.31.post9+tuxcare |
3.1.31.post17+tuxcare |
python-gitDebian:12 |
before 3.1.30-1+deb12u3, plus listed versions 3.1.30-13.1.30-1+deb12u2 |
3.1.30-1+deb12u3 |
python-gitDebian:13 |
before 3.1.44-1+deb13u1, plus listed versions 3.1.44-1 |
3.1.44-1+deb13u1 |
python-gitDebian:14 |
before 3.2.1-1, plus listed versions 3.1.44-13.1.45-13.1.46-13.1.50-13.1.61-13.1.62-13.1.62-23.1.62-3 |
3.2.1-1 |
gitpythonPyPI |
before 3.2.1, plus listed versions 0.1.70.2.0-beta10.3.0-beta10.3.0-beta20.3.1-beta2 Show 45 moreShow less0.3.20.3.2.10.3.2.RC10.3.30.3.40.3.50.3.60.3.71.0.01.0.11.0.22.0.02.0.12.0.22.0.32.0.42.0.52.0.62.0.72.0.82.0.92.0.9.dev02.0.9.dev12.1.02.1.12.1.102.1.112.1.122.1.132.1.142.1.152.1.22.1.32.1.42.1.52.1.62.1.72.1.82.1.93.0.03.0.13.0.23.0.33.0.43.0.5 |
3.2.1 |
python-gitUbuntu:Pro:14.04:LTS |
All versions, plus listed versions 0.3.2~RC1-20.3.2~RC1-30.3.2~RC1-3ubuntu0.1~esm10.3.2~RC1-3ubuntu0.1~esm20.3.2~RC1-3ubuntu0.1~esm3 |
None known |
python-gitUbuntu:Pro:16.04:LTS |
All versions, plus listed versions 1.0.1+git137-gc8b8379-11.0.1+git137-gc8b8379-21.0.1+git137-gc8b8379-2.11.0.1+git137-gc8b8379-2.1ubuntu0.1~esm11.0.1+git137-gc8b8379-2.1ubuntu0.1~esm21.0.1+git137-gc8b8379-2.1ubuntu0.1~esm4 |
None known |
python-gitUbuntu:Pro:18.04:LTS |
All versions, plus listed versions 2.1.5-12.1.6-12.1.7-12.1.8-12.1.8-1ubuntu0.1~esm12.1.8-1ubuntu0.1~esm22.1.8-1ubuntu0.1~esm4 |
None known |
python-gitUbuntu:Pro:20.04:LTS |
All versions, plus listed versions 2.1.11-13.0.4-13.0.5-13.0.7-13.0.7-1ubuntu0.1~esm13.0.7-1ubuntu0.1~esm23.0.7-1ubuntu0.1~esm4 |
None known |
Show 5 moreShow less
| Package | Affected | Fixed |
|---|---|---|
python-gitUbuntu:Pro:22.04:LTS |
All versions, plus listed versions 3.1.14-13.1.23-13.1.24-13.1.24-1ubuntu0.1~esm13.1.24-1ubuntu0.1~esm23.1.24-1ubuntu0.1~esm3 |
None known |
python-gitUbuntu:Pro:24.04:LTS |
All versions, plus listed versions 3.1.30-13.1.37-13.1.37-33.1.37-3ubuntu0.1~esm2 |
None known |
python-gitUbuntu:26.10 |
All versions, plus listed versions 3.1.46-13.1.50-13.1.61-1 |
None known |
python-gitUbuntu:Pro:26.04:LTS |
All versions, plus listed versions 3.1.44-13.1.45-13.1.46-13.1.46-1ubuntu0.1~esm1 |
None known |
python-GitPythonopenSUSE:Tumbleweed From an advisory covering 6 CVEs |
before 3.2.1-1.1 | 3.2.1-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected | ||
| affected, aliases, facts, references | OSV | |
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-114145 is a high vulnerability (CVSS 7.5) in Gitpython-developers Gitpython, published October 21, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.42%, the 36th percentile. Fixed in gitpython 3.2.1.
References
github.com/gitpython-developers/GitPython/security/advisories/GHSA-83vg-56qc-22m7
github.com/gitpython-developers/GitPython/pull/2264
github.com/gitpython-developers/GitPython/commit/97eadb888d2f99567e3113f4e455be4ca128f697
github.com/gitpython-developers/GitPython/releases/tag/3.2.1
nvd.nist.gov/vuln/detail/CVE-2026-114145
github.com/advisories/GHSA-83vg-56qc-22m7
Show 11 moreShow less
github.com/CVEProject/cvelistV5/tree/main/cves/2026/114xxx/CVE-2026-114145.json
security-tracker.debian.org/tracker/CVE-2026-114145
github.com/gitpython-developers/GitPython
ubuntu.com/security/CVE-2026-114145
cve.org/CVERecord?id=CVE-2026-114145
suse.com/security/cve/CVE-2026-110855
suse.com/security/cve/CVE-2026-110856
suse.com/security/cve/CVE-2026-110857
suse.com/security/cve/CVE-2026-110858
suse.com/security/cve/CVE-2026-110859
suse.com/security/cve/CVE-2026-114145
Left out here: 66 versions and the rest of 2 source descriptions. The JSON record has them.