GitHub Advisoryadvisory · PyPI
GHSA-284h-m62q-gf8w GitPython: Dormant multi-line git-config values are corrupted into live injected directives (e.g. core.hooksPath) on any unrelated GitConfigParser write, enabling RCE
CRITICAL
Official page ↗
- Aliases
- CVE-2026-78676
- Ecosystem
- PyPI
- Packages
- GitPython
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-88, CWE-94
- Reviewed
- 2026-09-08
Affected packages
• PyPI GitPython: < 3.1.59 → fixed in 3.1.59
Description
• CWE: CWE-88 (Argument Injection) / CWE-94 (Code Injection) — via a read-then-corrupt-on-rewrite config round trip, not a direct setter argument
• Affected component: `git/config.py` — `GitConfigParser._read()` (multi-line value decoding, lines 444-541, esp. `string_decode()` at line 460 and its call sites at 519/541) and `GitConfigParser._write()`/`write_section()` (serialization, lines ~694-712, esp. line 708)
• Affected version: GitPython at HEAD (`9729ed3b948f2bde09f1f188c5311e172212b67e`, 2026-08-05, VERSION `3.1.58`)
Reachability
GitPython added `UNSAFE_CONFIG_CHARS_RE` / `_value_to_string_safe()` / `_assure_config_name_safe()` guards (commits `c417af46`, `1ed1b924`, `a495ccd3`, and PR #2176) to reject a Python string containing a raw `\r`/`\n`/NUL byte, or syntax-bearing characters, when it is passed as an argument to `set()`, `set_value()`, `add_value()`, or `add_section()`. This closed the four config-injection GHSAs above.
That guard is applied only on the write-argument surface. It is never consulted for values that entered `GitConfigParser._sections` via `_read()` — i.e. values that came from parsing an on-disk config file. And `_read()` legitimately supports standard, spec-compliant git config syntax for multi-line values: a quoted value that is not closed on the same physical line continues onto the next physical line (git's own backslash-continuation syntax), and `string_decode()` (`.decode('unicode_escape')`) decodes a literal two-character `\n` escape sequence inside such a value into a real embedded LF character in the resulting Python string. No raw control byte is ever written to disk to achieve this — it's the same syntax real `git` itself uses and accepts.
The bug is in what happens when that `GitConfigParser` is later flushed: `write_section()` (line ~694) calls the *unsafe* `self._value_to_string(v)` — not `_value_to_string_safe()` — and "handles" any embedded newline in the value with `.replace("\n", "\n\t")` (line 708), emitting a bare, unquoted `` in the output file with no re-quoting and no backslash-continuation marker. Real git does not treat an indentation-only continuation the way GitPython's writer assumes — a value only continues across physical lines when the *previous* line ends in a literal `\` immediately before the newline. So the moment `write_section()` re-serializes a previously-decoded multi-line value this way, the second half of that value becomes an independent, new config line the next time anyone (GitPython or real `git`) parses the file. If an attacker chooses the dormant value's content to be `\nhooksPath = `, that second line is parsed as a brand-new `core.hooksPath = ` directive — live, real Git configuration, not a value.
`core.hooksPath` is honored by essentially every hook-firing git operation (`commit`, `checkout`, `merge`, `push`, `rebase`, ...), giving arbitrary code execution the next time the host application performs any hook-triggering operation.
Root cause
`GitConfigParser`'s injection guard is asymmetric: it hardens every *write-argument* entry point (the fix for the four sibling GHSAs) but never hardens the read → corrupt-on-rewrite round trip. A value that is 100% legitimate and inert as parsed from disk becomes a newly-injected directive purely through GitPython's own broken re-serialization logic (`write_section()` using the unsafe value-to-string path plus a continuation scheme real git doesn't recognize). The `c417af46` commit message even states its intent explicitly: *"This preserves existing read behavior for config files that already contain multiline values while preventing GitPython from writing new unsafe values"* — i.e. the maintainers consciously scoped the fix to the write-argument surface and did not address what happens when an already-resident multi-line value gets rewritten.
Exploit path
1. A `.git/config` (or any file merged into it via `[include]`, see below) already contains a dormant, syntactically-legitimate multi-line quoted value, e.g.:
```
[core]
zzz = "A\nhooksPath = ../evil-hooks\
"
```
No raw `\r`, `\n`, or NUL byte appears on disk — this is standard git quoting + backslash-continuation. Real `git config --get core.hookspath` returns nothing at this point (inert); `git config --get core.zzz` returns the decoded string `A\nhooksPath = ../evil-hooks`, identically to GitPython's own reader.
2. The host application opens this repo with GitPython (`git.Repo(path)`, `read_only=False` implicitly for a normal `config_writer()` use) and performs any single, unrelated, legitimate config write on the same `GitConfigParser` instance — e.g. `repo.config_writer().set_value("user", "name", "Test User")`. This is one of the most ordinary operations a GitPython-based tool performs.
3. `GitConfigParser._write()`/`write_section()` re-serializes every resident value, including the dormant `zzz` entry, using the unsafe path. The file on disk now contains, verbatim:
```
[core]
...
zzz = A
hooksPath = ../evil-hooks
```
4. Real `git config --get core.hookspath` now returns `../evil-hooks` — a key that did not exist before step 2, created purely by GitPython's own write.
5. The next hook-firing git operation (e.g. `git commit`) executes `../evil-hooks/pre-commit` (or whatever hook name the operation looks for), i.e. arbitrary attacker-chosen code execution.
Impact
Arbitrary code execution, on par with (and more directly triggered than) the already-accepted, High-severity `GHSA-mv93-w799-cj2w`/`GHSA-v87r-6q3f-2j67` "Newline injection... enables RCE via core.hooksPath" advisories, and requiring no unsafe caller argument at all — only an attacker-influenced config file plus one ordinary, unrelated write.
Preconditions
• A config file GitPython opens read-write already contains an attacker-chosen, syntactically-valid multi-line value shaped like `\n = `. Realistic delivery:
1. Pre-existing `.git` directory shipped with a repository — vendored/template repos, CI workspace/layer caches that preserve `.git`, "repo" tarball/zip distributions that include `.git/config`. The poisoned value sits directly in `.git/config`.
2. The documented shared-config `[include]` pattern (`[include] path = ../`, pointing at a file inside the working tree) — `GitConfigParser.read()` merges included files' sections into the same `_sections` dict used for writing, so a malicious public repository can ship the poisoned value inside a normal tracked file and have it activated the first time any GitPython-based tool performs any unrelated config write after clone (this requires the victim's own `.git/config` to already reference the include, e.g. via project setup tooling that adds `include.path`).
3. Any host application that opens an attacker-influenced config file for read-write and later performs a legitimate write — the exact trust-boundary the maintainers already accepted as realistic for `GHSA-v87r-6q3f-2j67` (their writeup cites MLRun's `project.push()`).
• No authentication/role requirement inside GitPython itself.
Evidence
• `git/config.py:460` (`string_decode`), invoked at `git/config.py:519` and `:541` inside `_read()`'s multi-line handling — decodes `unicode_escape`, turning a literal `\n` escape into a real embedded LF.
• `git/config.py:~694-712` (`_write()`/`write_section()`) — uses `self._value_to_string(v)` (unsafe variant) and `.replace("\n", "\n\t")` with no re-quoting.
• `c417af46` (the CR/LF/NUL guard commit) touches only the setter path and explicitly states it preserves existing *read* behavior for multi-line values, per its own commit message.
• `git log -S"string_decode"`, `-S"write_section"`, `-S'replace("\n", "\n\t")'` on `git/config.py` show these code paths have only ever been touched by non-security formatting/refactor commits (`a5fc1d86`, `b825dc74`, `cb6…
Related
Alias · CVE (1)
- CVE-2026-78676 GitPython before 3.1.59 fails to safely re-serialize multi-line git-config values during write operations, corrupting dormant quoted values… vulnerability
References
- https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-284h-m62q-gf8w web
- https://nvd.nist.gov/vuln/detail/CVE-2026-78676 advisory
- https://github.com/gitpython-developers/GitPython package
- https://github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3786.yaml web
- https://www.vulncheck.com/advisories/gitpython-before-remote-code-execution-via-config-injection web