CVE-2026-78676: How a Newline in a Git Config Value Becomes Remote Code Execution

A critical argument-injection flaw in GitPython lets a crafted multi-line config value corrupt into a live core.hooksPath directive, turning any unrelated config write into RCE. Here's the vulnerable pattern, the fix, and how the same bug class shows up whenever code writes structured config from untrusted strings.

On August 25, 2026, GitPython maintainers disclosed CVE-2026-78676 (GHSA-v87r-6q3f-2j67), a CVSS 9.3 critical vulnerability affecting every version before 3.1.59. The bug lives in config_writer().set_value(), the function countless CI scripts, git-hosting integrations, and dev-tooling wrappers use to programmatically edit a repository’s .git/config. It doesn’t just corrupt a value — under the right conditions it lets an attacker plant a live core.hooksPath directive, which git will happily execute the next time anyone runs commit, checkout, or merge in that repository.

What went wrong

Git config values can legitimately span multiple lines — a quoted value can contain embedded newlines, which git re-escapes as literal \n sequences when it writes them back out. GitPython’s set_value() failed to safely re-serialize multi-line values during write operations. If an attacker could get a crafted value — one containing a raw, unescaped newline followed by a new key = value pair — into a config write, GitPython would serialize it verbatim instead of escaping the embedded newline. The next time anything triggered a GitPython config write (even something unrelated, like setting a user’s display name), the corrupted value would flush to disk as two separate config lines instead of one escaped string.

That means an attacker who only controls one string value — a commit author name pulled from an untrusted webhook payload, a branch description, a CI-provided environment variable later written into config — can smuggle in a second, fully attacker-controlled directive:

[core]
    hooksPath = /tmp/.evil-hooks

Any subsequent git operation that fires a hook (post-checkout, pre-commit, post-merge) now executes whatever the attacker placed in that directory, with the privileges of whoever — or whatever CI runner — invoked git.

The vulnerable pattern

import git

def set_repo_author_name(repo_path: str, untrusted_name: str) -> None:
    repo = git.Repo(repo_path)
    with repo.config_writer() as cw:
        # VULNERABLE: untrusted_name is written verbatim; an embedded
        # newline + "key = value" pair becomes a real config directive
        # once GitPython re-serializes this (or any other) value.
        cw.set_value("user", "name", untrusted_name)

If untrusted_name arrives as:

Jane Doe\n[core]\n\thooksPath = /tmp/.evil-hooks

the resulting .git/config no longer contains one clean name = Jane Doe line — it contains a forged [core] section pointing hooks execution at an attacker-controlled directory. This is the same root cause as CRLF/HTTP response splitting or log injection: a lower-trust string is written into a structured, line-oriented format without escaping the delimiter (here, \n) that separates one directive from the next. In the security literature this is CWE-88, Argument Injection, and OWASP maps it under A03:2021 — Injection.

The fix

Upgrade to GitPython 3.1.59 or later, which correctly escapes embedded newlines during re-serialization. Don’t stop there — treat any value flowing into config_writer() (or any git-config-writing call in any language/library) as untrusted input that needs validation before it touches disk:

import re
import git

CONTROL_CHARS = re.compile(r"[\r\n\x00]")

def set_repo_author_name(repo_path: str, untrusted_name: str) -> None:
    if CONTROL_CHARS.search(untrusted_name):
        raise ValueError("config value contains control/newline characters")

    repo = git.Repo(repo_path)
    with repo.config_writer() as cw:
        cw.set_value("user", "name", untrusted_name)

    # Defense in depth: verify what actually landed on disk matches
    # what we intended to write, catching any re-serialization bug.
    with repo.config_reader() as cr:
        if cr.get_value("user", "name") != untrusted_name:
            raise RuntimeError("config value did not round-trip cleanly")

The same discipline applies outside Python. Any code that writes .npmrc, .gitconfig, .editorconfig, or similar line-delimited config formats from a string that ultimately traces back to a user, webhook, or external API needs the same newline/control-character rejection before it ever reaches a serializer:

// Node.js: reject embedded newlines before writing any INI-style config value
function assertSafeConfigValue(value) {
  if (/[\r\n\0]/.test(value)) {
    throw new Error("Refusing to write config value containing control characters");
  }
  return value;
}

fs.appendFileSync(
  path.join(repoPath, ".git", "config"),
  `\tname = ${assertSafeConfigValue(untrustedName)}\n`
);

Checklist for your own code

  • Upgrade GitPython to >=3.1.59 immediately if you programmatically write git config — CI orchestration tools, git-hosting integrations, and pre-commit frameworks are common consumers.
  • Treat any function that serializes a string into a structured, delimiter-based format (INI, config files, HTTP headers, log lines, CSV) as an injection sink; reject or escape the delimiter character before the value reaches it, don’t rely on the library to do it for you.
  • Audit for core.hooksPath, core.sshCommand, and core.fsmonitor specifically — these are the config keys most commonly abused to turn a config-write primitive into code execution, since git invokes them as commands.
  • Add a read-back verification step after any config write in security-sensitive tooling: if what you read doesn’t match what you wrote, treat it as a signal something re-serialized incorrectly.
  • Don’t source config values — even seemingly cosmetic ones like a display name — directly from external, attacker-influenced input (webhooks, PR metadata, commit trailers) without validation, regardless of which library or language is doing the writing.