Skip to content

[WIP] Fix GitPython section-name injection vulnerability - #24

Merged
Arun Iyer (aruniyer) merged 1 commit into
mainfrom
copilot/fix-gitpython-config-injection
Aug 6, 2026
Merged

[WIP] Fix GitPython section-name injection vulnerability#24
Arun Iyer (aruniyer) merged 1 commit into
mainfrom
copilot/fix-gitpython-config-injection

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
  • Update GitPython pin in requirements.txt from 3.1.52 to 3.1.53 (patched version)
  • Verify no submodule-related APIs (create_submodule, submodule_update, Submodule.add) are used anywhere in the codebase
  • Installed GitPython==3.1.53 locally and confirmed import git works and resolves cleanly with pip install

Reachability Assessment

The advisory (GHSA-3rp5-jjmw-4wv2) describes a git-config section-name injection vulnerability that is exploitable specifically through GitPython's submodule APIs: Repo.create_submodule(name=...) and Repo.clone_from(...) + repo.submodule_update(init=True), where an attacker-controlled submodule name is written unescaped into .git/config.

I searched the entire repository for GitPython usage and found only two files that import git (the GitPython package):

  • project_utils/csharp_setup_utils.py — uses git.Repo.clone_from(...) (fixed, trusted URL) and git.Repo.init(...).
  • repoclassbench/dataset/python_setup_utils/git_related_utils.py

Neither file calls create_submodule, submodule_update, Submodule.add, or any other submodule-related API, and no .gitmodules-processing logic exists in the codebase. There is no repository code path that constructs a submodule from user-controlled or untrusted names.

Conclusion: the vulnerable code path is not reachable in this codebase (high confidence, since the advisory names specific APIs — create_submodule/submodule_update/Submodule.add — and a full-repo search confirms none of them are called). The version bump to the patched release (3.1.53) is applied to resolve the Dependabot alert and satisfy vulnerability scanners rather than to address an active exploitation risk in this repository.

No other code changes were required since the affected functionality is unused.

Original prompt

This section details the Dependabot vulnerability alert you should resolve

<alert_title>GitPython: git-config section-name injection enables arbitrary config directives (core.sshCommand RCE)</alert_title>
<alert_description>### Summary

In GitPython <= 3.1.52, the config writer neutralizes only CR, LF, and NUL in configuration names, but writes section names into the [...] header with no other escaping. A section/subsection name that contains ] [ " closes the intended header and opens a second same-line section, injecting an arbitrary config directive — with no newline required. Because a submodule name is attacker-controlled data (it comes from a repository's .gitmodules, or from an application that lets a user name a submodule) and is written verbatim into the parent repository's trusted .git/config, an attacker can set core.sshCommand (or alias.*, core.pager, core.fsmonitor) and achieve remote code execution on the victim's next git operation. Likely CWE-74 (Injection).

This is a distinct variant of the injection addressed by GHSA-mv93-w799-cj2w / GHSA-v87r-6q3f-2j67: those fixed newline injection into config values/names (patched in 3.1.50); the [r\n\x00] guard added for them does not stop a same-line section break inside a name.

Details

The only guard applied to section/option names before writing is _assure_config_name_safe, which uses a regex that matches solely CR/LF/NUL:

git/config.py:75,897-899 (GitPython 3.1.52):

UNSAFE_CONFIG_CHARS_RE = re.compile(r"[\r\n\x00]")
...
def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None:
    if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):
        raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label)

The name is then serialized into the header with no escaping of ], [, ", space, = or #:

git/config.py:693:

fp.write(("[%s]\n" % name).encode(defenc))

For submodules the name is wrapped as submodule "<name>" (git/objects/submodule/util.py:39, return f'submodule "{name}"'), which supplies the balancing quote. A submodule named:

x"] [core] sshCommand=CMD #

therefore serializes to the header [submodule "x"] [core] sshCommand=CMD #"]. git parses everything after the first ] on that line as a fresh section, yielding core.sshCommand=CMD (the trailing #"] is an inline comment). No CR/LF/NUL appears, so _assure_config_name_safe never fires.

The attacker-controlled name reaches this sink through documented public entry points that write it into the parent repository's .git/config:

  • Repo.create_submodule(name=<untrusted>, ...)Submodule.addgit/objects/submodule/base.py:619 writer.set_value(sm_section(name), "url", url) — a single call, no hostile remote required.
  • Repo.clone_from(<hostile url>) + repo.submodule_update(init=True)git/objects/submodule/base.py:855 writer.set_value(sm_section(self.name), "url", self.url), where self.name is read unvalidated from the cloned repo's .gitmodules.

Asymmetry: the sibling class is blocked — a newline in a config value, e.g. set_value("core", "editor", "x\n\tsshCommand=CMD"), raises ValueError. The section-name bracket payload is not caught by the same guard.

PoC

Single self-contained script, run against the pinned release in an ephemeral environment. Non-destructive: the injected value is an inert marker, verified parse-only with git config --get; no ssh/fetch/push is run and nothing is executed.

#!/usr/bin/env python3
"""Minimal PoC: git-config section-name injection in GitPython==3.1.52."""
from importlib.metadata import version
import os, tempfile, subprocess
import git

print(f"# GitPython {version('GitPython')}")        # version proof -- first line

MARKER = "MARKER_9f3a"                               # inert; never executed
tmp = tempfile.mkdtemp()
env = {**os.environ, "HOME": tmp,
       "GIT_CONFIG_GLOBAL": os.path.join(tmp, "gc"), "GIT_CONFIG_SYSTEM": os.devnull,
       "GIT_AUTHOR_NAME": "a", "GIT_AUTHOR_EMAIL": "a@b.c",
       "GIT_COMMITTER_NAME": "a", "GIT_COMMITTER_EMAIL": "a@b.c"}

def run(*a, cwd=None):
    return subprocess.run(a, cwd=cwd, env=env, capture_output=True, text=True)

# A benign local repo used as the submodule url (a plain path, no network).
src = os.path.join(tmp, "src"); os.makedirs(src)
run("git", "init", "-q", src)
open(os.path.join(src, "f"), "w").write("x")
run("git", "add", "f", cwd=src); run("git", "commit", "-qm", "i", cwd=src)
suburl = os.path.join(tmp, "sub.git"); run("git", "clone", "-q", "--bare", src, suburl)

def parent_repo():
    p = tempfile.mkdtemp(dir=tmp)
    run("git", "init", "-q", p)
    open(os.path.join(p, "r"), "w").write("x")
    run("git", "add", "r", cwd=p); run("git", "commit", "-qm", "i", cwd=p)
    return p

def injected_sshcommand(parent):
    r = run("git", "config", "-f", os.path.join(parent, ".git", "co...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Resolves microsoft/repoclassbench alert #60

@aruniyer
Arun Iyer (aruniyer) marked this pull request as ready for review August 6, 2026 08:23
@aruniyer
Arun Iyer (aruniyer) merged commit 8cd1306 into main Aug 6, 2026
4 of 5 checks passed
@aruniyer
Arun Iyer (aruniyer) deleted the copilot/fix-gitpython-config-injection branch August 6, 2026 08:23
Copilot stopped work on behalf of Arun Iyer (aruniyer) due to an error August 6, 2026 08:23
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.

2 participants