CVE-2026-87817: GitPython before 3.1.60 Remote Code Execution via Git Directory Impersonation

Published Sep 9, 2026
·
Updated

Summary

Repo.init decides which directory is the git directory by testing candidate paths in an order that considers the real .git last. Two earlier tests can be satisfied by ordinary tracked files. Git reserves only the literal name .git, so HEAD, objects/, refs/, config, gitdir, commondir and hooks/ at a repository root are all legal tracked content.

Consequently, after a victim opens or clones an attacker's repository, GitPython resolves gitdir to the working-tree root while real git correctly resolves <root>/.git. Everything GitPython then treats as "inside the git directory" is attacker-authored content — including hooks/, which it executes.

CVE-2026-87817

Affected code (3.1.59)

The discovery loop in git/repo/base.py tests, in order:

1. git/repo/base.py:299 — isfile(curpath/gitdir) and isfile(curpath/commondir) and isfile(curpath/HEAD) 2. git/repo/base.py:320 — isgitdir(curpath) 3. git/repo/base.py:341 — dotgit = osp.join(curpath, ".git") ← the real git dir, considered last

isgitdir (git/repo/fun.py:60) requires only that objects/ and refs/ are directories and that HEAD is a file; HEAD's contents are never parsed. The hook path is resolved from index.repo.gitdir (git/index/fun.py:73), i.e. the mis-resolved directory.

Proof of concept

Requires only pip install GitPython==3.1.59. Full script attached as poc1rce.py; it runs entirely in a temp directory and the payload only writes a marker file.

Attacker repository — four ordinary tracked files at the root:

| path | mode | content | |---|---|---| | gitdir | 100644 | .git\n | | commondir | 100644 | .git\n | | HEAD | 100644 | ref: refs/heads/master\n | | hooks/pre-commit | 100755 | #!/bin/sh + payload |

Victim — two ordinary calls:

python repo = git.Repo.clonefrom(url, dst) # or git.Repo(dst) repo.index.commit("automated commit") # code execution happens here

Observed on the PyPI release 3.1.59 (Linux and Windows):

real git says the git dir is : /tmp/.../victim/.git GitPython says it is : /tmp/.../victim <- shadowed attacker's hook executed : True git fsck : (clean)

The pre-commit hook runs at git/index/base.py:1201, before writetree(), so it fires even though the commit later fails.

Impact

A service that opens or clones an untrusted repository with GitPython — a CI runner building a fork pull request, a code-scanning/SBOM service, a mirror, a dependency bot, an AI code-review/agent tool — can be made to:

1. Execute arbitrary commands via the tracked <root>/hooks/pre-commit when the victim calls index.commit(). 2. Read files outside the repository: the tracked <root>/config becomes the repository config and is parsed with mergeincludes=True (git/repo/base.py:765), so [include] path = ~/.aws/credentials discloses the file. (Repo.configreader still defaults mergeincludes=True, so the hardening added in 3.1.59 for .gitmodules/GHSA-7833 does not cover this path.) 3. Write a config file to an attacker-chosen directory via an absolute tracked commondir.

Delivery is silent: git clone exits 0, git fsck (including --strict, and with transfer.fsckObjects/fetch.fsckObjects=true) reports nothing, and the clone passes every read-only probe (head.commit, branches, isdirty(), untrackedfiles, itercommits) because a tracked commondir of .git pins commondir to the real .git.

Threat model / preconditions

- Attacker controls the content of a repository the victim opens or clones with GitPython (public repo, fork PR, mirrored dependency). - For code execution, the victim performs a commit via GitPython's native index.commit(). For the file-read impact, opening the repo and reading config is enough. - GITWORKTREE is not a mitigation — gitdir is assigned and the discovery loop breaks before the environment is consulted. - Real git is unaffected; only GitPython mis-resolves the directory.

Remediation

1. Test curpath/.git before the gitdir/commondir/HEAD triple and before isgitdir(curpath). 2. Resolve hookpath / commithookpath / getvalidatedreflogpath through repo.commondir. 3. Containment-check commondir/gitdir contents before joining (reuse SymbolicReference.getvalidatedpath). 4. Validate HEAD in isgitdir (require ref: refs/... or a 40-hex sha). 5. Pass mergeincludes=False in Repo.configreader (git/repo/base.py:765).

Note for the maintainers

Commit 406b98e1 (2026-05-31, "respect core.hooksPath for commit hooks") resolved the hook path via git rev-parse --git-path, which is immune because git rediscovers the true .git. Commit 9bc287a2 (2026-07-20) reverted it to avoid an unconditional rev-parse dependency — reintroducing the exec step. Measured at each commit with the healthy-looking layout: 406b98e1 → hook did not fire; 9bc287a2 … 3.1.59 → hook fired.

Scope note

Only the repository-root case is reported: where a real .git exists, git prefers it, and GitPython does not. A fake git dir in a subdirectory fools real git too, so that variant is out of scope as a general ecosystem hazard rather than a GitPython defect.

###POC Files : poc1rce.py poc2fileread.py

Other sources

GitPython before 3.1.60 fails to properly validate the git directory location, allowing attackers to impersonate the git directory using tracked files like gitdir, commondir, and HEAD. Attackers can execute arbitrary code by placing a malicious pre-commit hook in the tracked hooks directory that executes when a victim calls index.commit() on a cloned or opened repository.

— MITRE

Affected Software

3 affected componentsFixes available
GitPython GitPython<3.1.60
Gitpython Project Gitpython Python<3.1.60
pip/gitpython<=3.1.59
3.1.60

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.1.60
  2. Upgrade

    Upgrade GitPython to a version that resolves this vulnerability.

    Fixed in 3.1.60
  3. Configuration

    Pass merge_includes=False to Repo._config_reader to prevent configuration includes from being processed.

    GitPython Repo._config_reader merge_includes = False
  4. Compensating control

    Harden GitPython repository discovery by testing curpath/.git before the gitdir/commondir/HEAD checks and before is_git_dir(curpath), validating HEAD to require ref: refs/... or a 40-hex SHA, containing commondir/gitdir contents before joining them, and resolving hook_path, _commit_hook_path, and _get_validated_reflog_path through repo.common_dir.

Event History

Sep 9, 2026
CVE Published
via MITRE·11:21 AM
Data Sourced
via MITRE·11:21 AM
DescriptionSeverityWeakness
Data Sourced
via NVD·12:17 PM
DescriptionSeverityWeaknessAffected Software
Sep 30, 2026
Advisory Published
via GitHub·11:27 PM
Data Sourced
via GitHub·11:27 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to exploitation?

Applications using GitPython versions before 3.1.60 are exposed when they clone or open a repository whose tracked files can be attacker-controlled, and then call index.commit() on that repository.

2

What must an attacker provide for code execution to occur?

The attacker needs to place files such as gitdir, commondir, or HEAD that impersonate the Git directory and provide a malicious pre-commit hook in the tracked hooks directory. The hook executes when the victim invokes index.commit().

3

Does exploitation require user interaction or authentication?

The supplied severity vector indicates network attack access, low complexity, no privileges required, and user interaction required. The described triggering action is the victim calling index.commit() on the cloned or opened repository.

4

What version resolves the issue?

GitPython 3.1.60 resolves the issue; versions before 3.1.60 are affected.

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