The Guard Read the Keywords, the Attacker Used Positionals: GitPython’s Six-CVE 3.2.0 Security Release
Six CVE records — CVE-2026-110855 through CVE-2026-110860 — were published on 14 October 2026 between 23:17:26 and 23:17:27 UTC against GitPython (5,185 stars, 1,006 forks, BSD-3-Clause), the Python library that half the automation ecosystem uses to drive git. All six name the same fix: before 3.2.0. Version 3.2.0 is explicitly titled a Security release and shipped on 30 September 2026 (PyPI upload 09:10 UTC, GitHub tag 09:12 UTC); current is 3.2.1, dated 14 October. The CNA on all six records is VulnCheck, and every record carries NVD status Received.
GitPython is not an AI product, and this briefing covers it for the same reason it covered Komodo’s Periphery agent: it is the thing underneath the AI product. Coding agents clone repositories they did not choose. Automation fetches refs it did not write. Every agentic coding loop, every acquisition pipeline, every “analyse this repo” feature eventually calls something like Repo.clone() on input selected by someone else — frequently by the model, occasionally by the adversary. These six records are what that call can do back.
Three ways a string becomes an option
The batch contains three distinct shapes of the same defect — an attacker-influenced string reaching git’s argument vector where an option is accepted — and the differences between them are the education.
CVE-2026-110857 (7.5, CWE-1287) is the bluntest: Git.ls_remote forwards URLs without check_unsafe_protocols validation, so an ext:: remote-helper URL executes commands where git permits the ext transport. The same record notes merge_base and IndexFile.move accepting unguarded option injection. The URL was treated as data; git treated its scheme as a dispatch instruction. Any application that resolves user- or model-supplied repository URLs through this API hands the URL’s author a command channel gated only by whether the local git build permits the transport.
CVE-2026-110858 (8.1, CWE-88), the highest-scored of the six, is the subtlest and the one this briefing is named for. Remote.pull() has an unsafe-option guard — and the guard inspects only keyword arguments, not the positional refspec. An attacker controlling the refspec supplies --upload-pack=<command> and gets OS command execution as the calling process over local-path or SSH transports. The check existed, it ran, and it examined the part of the call the attacker was not using. Readers of our account of n8n’s fourteen-CVE batch — published the same evening, including a Git-node code-execution record of its own — will recognise the motif instantly: the gate and the execution disagreed about what was being looked at.
CVE-2026-110859 (7.5, CWE-88) completes the triptych with the strangest carrier: a branch name. Head.checkout() parses the branch as a git option, so a repository whose default branch is named --pathspec-from-file=<path> causes cloning applications that check out the active branch to leak arbitrary file contents through GitCommandError messages. The error message is the exfiltration channel: the application fails safely, and the failure text contains the file. Branch names are attacker-controlled strings that cross trust boundaries inside ordinary protocol operation — fetch advertises them, clone checks them out — and almost nothing treats them as hostile, for the same reason nobody treated file names as hostile until eighteen CVEs arrived in nineteen seconds.
The other three: secrets, CPU, and the symlink
CVE-2026-110855 (6.5, CWE-200) is the record operators should read most carefully despite its middling score. Repo._clone() omits expand_vars=False when calling Git.polish_url() for separate_git_dir, so a crafted .gitmodules submodule name containing variable syntax expands environment variables into .git/modules directory names and DEBUG logs. The threat model is not abstract: CI runners and agent sandboxes are dense with secrets in the environment — tokens, keys, credentials the job needs — and a repository that any of them clones can transcribe those values into paths and log lines, which then persist in artefacts, caches and log aggregators long after the job ends. Environment-variable expansion is a write-what-you-read primitive aimed at exactly the process state automation is worst at keeping clean.
CVE-2026-110856 (7.5, CWE-1333) is a regular-expression denial of service in the _re_actor_epoch pattern used by parse_actor_and_date on author, committer and tagger lines. Commit or tag objects with long space-filled lines lacking a valid epoch or timezone trigger quadratic backtracking and CPU exhaustion. The attacker’s cost is one malicious repository served once; the victim’s cost is a pinned core per parse, in a library whose callers routinely parse every object in a fetched history.
CVE-2026-110860 (6.1, CWE-59) is a symlink-following flaw in Submodule.move() (introduced in the 3.1.62 range, fixed in 3.2.0): the destination is checked only lexically, without rejecting symbolic links in intermediate path components, so a committed symlink such as vendor pointing outside the worktree relocates the checkout externally and corrupts the index and .gitmodules. Lexical validation of a path that the filesystem will resolve differently is the path-traversal equivalent of checking the wrong field — the string that was verified is not the location that gets written.
The timeline, and the company it keeps
- 30 September 2026, 09:10–09:12 UTC — GitPython 3.2.0, titled Security, is published to PyPI and GitHub.
- 14 October 2026, 12:23 UTC — 3.2.1 follows.
- 14 October 2026, 23:17 UTC — all six CVE records go public, fourteen days after the security release.
Fourteen days, again — the same interval as n8n’s batch the same evening, and the same fixed-but-unlabelled-for-scanners window we have now documented for Komodo, NVIDIA, and PraisonAI. GitPython at least titled the release “Security,” which is more signal than a Misc. changelog bullet, but release titles are not CVE identifiers and scanner matching still waited a fortnight. One mitigation note the timeline makes visible: unlike the batches where the fix predates any usable signal, here the fixed version number (3.2.0) was public the whole time — dependents pinning GitPython>=3.1 floated to safety automatically while pinned ones did not, which is the entire argument for floating security-sensitive transitive dependencies stated as an experiment result.
What to do
Upgrade to GitPython 3.2.0 or later (current 3.2.1); the fix is the whole remediation and there is no per-CVE workaround worth preferring. Then do the two things the records imply but do not state. First, audit where repository-controlled strings enter your git calls: URLs into ls_remote/clone, refspecs into fetch/pull, branch names into checkout — and treat all three as option-injection carriers until the library version is confirmed, because 110857, 110858 and 110859 are three independent proofs that each position was reachable. Second, if any automation cloned untrusted repositories with a vulnerable version in DEBUG logging or with secrets in the environment, scope 110855 as a past-exposure record: rotate what was in the environment, not just the library, since expanded values persist in logs and directory names the upgrade does not scrub.
And the generalisable rule, with Komodo’s -- separator as the contrast: Komodo’s fix worked because the dangerous values were arguments, and separators plus escaping demote arguments back to data. GitPython’s three injection records are the cases where that remedy does not apply — --upload-pack=<command> and --pathspec-from-file=<path> are well-formed options, not confused arguments, and no amount of quoting disarms an option the program was designed to accept. When the attacker’s string arrives in a position the callee parses as instructions, the only fix is to stop the string from arriving: validate the scheme, validate the refspec, never let a branch name near an option parser. Escaping defends data positions. Validation defends instruction positions. Knowing which position you are passing into is the whole discipline.
Reporting note: all six CVE IDs, publication timestamps, CVSS scores, CWE identifiers, descriptions, affected-version statements and advisory references were read first-hand from the NVD 2.0 API (CNA: VulnCheck; all records status Received at time of writing). Release titles and timestamps were read first-hand from the GitHub releases API and the PyPI JSON API; repository metadata (5,185 stars, 1,006 forks, BSD-3-Clause) from the GitHub repository API. Nothing was installed, no repository was cloned, no refspec, branch name or submodule was crafted, no git command was executed, and no payload was sent. Brave Search returned only CVE-mirror aggregators; no vendor advisory beyond the release itself and no independent technical analysis was available at the time of writing.
Sources:
- NVD — CVE-2026-110855, environment-variable disclosure via separate_git_dir (GHSA-fx3j-rwgx-fr94)
- NVD — CVE-2026-110856, ReDoS in _re_actor_epoch (GHSA-m64x-33q8-m5h7)
- NVD — CVE-2026-110857, ext:: RCE via ls_remote (GHSA-w8jc-g24h-crhw)
- NVD — CVE-2026-110858, --upload-pack injection via positional refspec (GHSA-f9j4-qggq-h239)
- NVD — CVE-2026-110859, arbitrary file read via branch name (GHSA-23mf-xhv8-69c2)
- NVD — CVE-2026-110860, Submodule.move() symlink escape (GHSA-gq48-pqfc-9p58)
- GitPython 3.2.0 release (30 September 2026) — the Security release
- PyPI — GitPython 3.2.0 and 3.2.1 upload timestamps