SevyDevy

Guide · Backend

GitPython's October 2026 Submodule Flaw Explained: Validate Before You Clone

14 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The GitPython vulnerability GHSA-83vg-56qc-22m7 let create_submodule clone a submodule into the live .git/hooks folder before it rejected the path. GitPython is a Python library that drives git.
  • A git hook is a script git runs on an event like checkout. An executable file named post-checkout in .git/hooks runs on the next checkout.
  • On 3.2.0 the path check raised a ValueError, but only after the clone had already written the file. On 3.1.x there was no such check at all.
  • The fix is GitPython 3.2.1, on PyPI on 14 October 2026. It now rejects a .git alias before touching the disk. The repo advisory lists no CVE, but NVD and the GitHub Advisory Database list CVE-2026-114145.
  • Beginners can stop after why the order of the checks matters. Engineers get the exact 3.2.0 versus 3.2.1 code paths and a one-minute version check.

What goes wrong when a program trusts a submodule path?

On GitPython 3.2.0 and earlier, a submodule path taken from user input can plant a script that git later runs. This is the GitPython vulnerability GHSA-83vg-56qc-22m7, also listed as CVE-2026-114145.

The bug sits in one order-of-operations mistake. GitPython checked whether a submodule path was safe, but only after it had already cloned files to that path on disk.

Picture a common setup. A platform team runs a service that builds a workspace from a manifest a user uploads. The manifest lists submodules to add, each with a name, a path and a url.

The service loops over that list and calls create_submodule for each entry. The path comes straight from the user's manifest. The setup is made up, but the bug is real.

An attacker sets the path to .git/hooks and points the url at a repository they control. That repository holds one file at its root, named post-checkout, marked executable. From here the order of the checks decides everything.

Which words do you need before reading this advisory?

You need five terms: submodule, git hook, create_submodule, the .git directory, and the validate-before-mutation rule. Each gets a plain meaning below.

Submodule. A submodule is one git repository checked out inside another. You use it to pull in a shared library and pin it to a known commit.

Git hook. A git hook is a script that git runs automatically on an event. Hooks live in the .git/hooks folder, named by the event, such as post-checkout or pre-commit.

create_submodule. This is the GitPython method your code calls to add a submodule. It takes a name, a path and a url, then clones the repository at the url into the path.

The .git directory. Every git repository keeps its internals in a hidden folder called .git. It holds the history, the config and the hooks. It is not meant to be a checkout target.

Validate before mutation. This is the rule the fix restores. Check that an input is safe before you change any file on disk, never after. The two must not swap order.

What is the GitPython vulnerability in create_submodule?

It is a check-then-act ordering bug. The path .git/hooks passed the early check, so GitPython cloned the attacker's repository into the live hooks folder. Only later did a stricter check object.

The cloned file post-checkout now sits in .git/hooks as an executable file. Git treats any executable file there with a known hook name as a hook. The next checkout runs it.

That is the arbitrary code execution. The attacker never ran code directly. They placed a file where git will later run it, and let a normal git operation pull the trigger.

Pause and predict: 3.2.0 does have a strict path check. Where in the flow must it run to stop the attack? Keep your answer in mind for the diagram.

The diagram below follows the manifest through GitPython 3.2.0. Watch the clone box come before the check box. That single swap is the whole bug.

How a bad path slipped through on 3.2.0

Notice the order: the clone writes to disk first, and the rejecting check runs only afterwards.

Diagram connections:

  • User manifest to Your service
  • Your service to Early path check (path .git/hooks)
  • Early path check to Clone the submodule (passes early check)
  • Clone the submodule to Index-time check (after writing disk)
  • Index-time check to post-checkout remains (error comes too late)
  • post-checkout remains to Next checkout (file stays behind)
  • User manifest: path set to .git/hooks, attacker url
  • Your service: calls create_submodule(name, path, url)
  • Early path check: 3.2.0 accepts .git aliases here
  • Clone the submodule: writes files into .git/hooks on disk
  • Index-time check: raises ValueError, but the file is written
  • post-checkout remains: now an executable hook in .git/hooks
  • Next checkout: git runs the hook as code

One detail softens the risk. Git refuses to clone into a folder that is not empty. So the .git/hooks folder must be empty or missing, or nothing is planted. More on that below.

Why did the ValueError come too late on 3.2.0?

Because the strict check ran at index time, when git stages the change. That is after the clone, so the write had already happened. GitPython had two checks that disagreed on when to test the path.

In the source, create_submodule calls Submodule.add. That method first normalises the path with a helper called _to_relative_path. On 3.2.0 this helper did not reject a .git alias.

Next, add checked the checkout path with _checked_abspath. On 3.2.0 that helper only rejected symbolic links. A plain path like .git/hooks went straight through it.

Then add cloned the repository. Only at the very end, when it wrote the entry to the index, did a strict helper called _validate_repo_path finally reject the .git alias and raise ValueError.

So the error was real, but the damage was done. The file had already been written to .git/hooks before the error was raised. The advisory calls this CWE-696, incorrect behaviour order.

How do the affected versions differ from the fix?

The three lines behave in three ways. The 3.1.x line had no path check, 3.2.0 checked too late, and 3.2.1 checks before any clone. The table sets them side by side.

What happens when create_submodule gets a .git/hooks path (empty hooks folder)
Version linePre-clone path checkResult with a .git/hooks pathStatus
3.1.x, up to 3.1.62NoneClones into .git/hooks, no error raisedAffected
3.2.0Symlinks onlyClones first, then ValueError at index timeAffected
3.2.1 and laterFull, before any cloneRejected before a single file is writtenFixed

We checked this behaviour on the real released packages. We did not plant any hook or clone anything. We only called the pre-clone path helpers with the harmless string .git/hooks.

On 3.1.62 and 3.2.0, the helper _to_relative_path accepted .git/hooks and returned it unchanged. On 3.2.1 the same call raised: Repository path aliases Git metadata. The check moved to the front.

What did the GitPython 3.2.1 fix actually change?

It moved the strict validation to the front, before any clone. The fix commit is titled validate destinations before mutation, which names the rule it restores.

Three changes matter most. First, _to_relative_path now calls _validate_repo_path, so a .git alias is rejected during path normalisation, before the clone.

Second, _checked_abspath now compares the target against the repository's git directories by filesystem identity. A path that aliases .git is refused, not just a symbolic link.

Third, after a clone, the code re-checks the path. If a parallel clone turned a parent folder into a git directory, it removes the clone's HEAD and raises. This closes a race.

The diagram below shows the fixed order. The decision box now sits before the clone, so an unsafe path stops the work with nothing written to disk.

The fixed order in GitPython 3.2.1

Notice the check is now a gate before the clone. An unsafe path exits with no file on disk.

Diagram connections:

  • User manifest to Your service
  • Your service to Validate the path
  • Validate the path to Does it alias .git?
  • Does it alias .git? to Raise an error (Yes)
  • Does it alias .git? to Clone the submodule (No)
  • Clone the submodule to Submodule added
  • User manifest: path and url from the user
  • Your service: calls create_submodule(name, path, url)
  • Validate the path: runs before touching the disk
  • Does it alias .git?: compared by filesystem identity
  • Raise an error: nothing is cloned or written
  • Clone the submodule: only for safe paths
  • Submodule added: index updated as normal

How serious is this GitPython vulnerability in practice?

It is rated High, but it is not a one-click internet exploit. The advisory's CVSS vector is AV:N/AC:H/PR:N/UI:R, which means network reach but high attack complexity and required user interaction.

Several things must line up. The attacker must control the path your code passes to create_submodule. That is the key condition, and it does not hold for most code.

The .git/hooks folder must also be empty or missing. Git clone refuses a non-empty destination, so if the usual sample hooks are present, nothing is planted. Empty hook folders are common in CI, containers and minimal checkouts.

Finally, a git operation must run later to trigger the hook, such as a checkout. If all of that holds, the result is arbitrary code execution, which is why the score is 7.5.

How do you upgrade and check the fix in 2026?

Move to GitPython 3.2.1 or later, then confirm the version in your environment. The steps below work the same on a laptop, a server or a CI runner.

  1. Upgrade the package

    Run pip install --upgrade GitPython to pull 3.2.1 or later. In a locked project, pin GitPython to a version of 3.2.1 or above.

  2. Mind the Python floor

    The 3.2.x line needs Python 3.8 or newer, since 3.2.0 dropped Python 3.7. Upgrade your Python first if you are still on 3.1.x with 3.7.

  3. Confirm the installed version

    Run the version command below. It should print 3.2.1 or higher. Anything at 3.2.0 or below is still affected.

  4. Review your path inputs

    Search your code for create_submodule and Submodule.add. Check where each path comes from, and reject any path that touches .git yourself as a second layer.

bash
python -c "import git; print(git.__version__)"
# Expect 3.2.1 or higher. 3.2.0 or below is affected.

There is no safe workaround that keeps an old version. The advisory lists none. If you cannot upgrade at once, make sure no untrusted value ever reaches the path argument.

What is the wider lesson for your own code?

Validate before you mutate. Any check that runs after a write cannot undo the write. This is true for files, databases, payments and sent emails alike.

When two checks guard the same action, make the strict one run first. Here, a loose early check and a strict late check disagreed, and the gap between them was the bug.

Treat any path built from user input as hostile. A string like .git/hooks looks harmless, yet it points at a place where code gets run. Reject reserved locations before you act.

Last, remember that git hooks are code. Anything that can write a file into .git/hooks can run commands on the machine. Protect that folder as carefully as you protect a login.

How do you explain the GitPython vulnerability in a system design interview?

Frame it as a check-then-act ordering bug that leads to code execution. Then walk through the chain of causes, and end on the fix.

  1. Name the root cause

    Say GitPython validated a submodule path only after cloning into it. The clone wrote to disk before the rejecting check ran.

  2. Show the dangerous path

    A path of .git/hooks let the clone drop a post-checkout file into the live hooks folder. That file is then a git hook.

  3. Explain the trigger

    Git hooks are scripts git runs on events. The next checkout runs the planted hook, so writing a file became running code.

  4. Scope the risk honestly

    It needs an attacker-controlled path, an empty .git/hooks folder and a later git operation. High severity, but high complexity, not trivial.

  5. Give the fix and the rule

    Version 3.2.1 validates the path before any clone. The general rule is validate before mutation: never let a write happen before its check.

This answer shows three things interviewers like. You know the mechanism, you rank the risk without hype, and you state the rule that prevents the whole bug class.

Questions people ask

Which GitPython versions are affected by GHSA-83vg-56qc-22m7?
All versions of 3.2.0 and earlier are affected, which includes the whole 3.1.x line up to 3.1.62. The fix is in 3.2.1 and later, released on PyPI on 14 October 2026. As of 28 October 2026 there is no upstream 3.1.x backport, so upgrade to 3.2.1 or higher and confirm the installed version. Linux distribution packages follow their own schedule, so check your distribution's security tracker too.
Am I at risk if I use GitPython but never call create_submodule?
The advisory describes this bug in create_submodule, which maps to Submodule.add. If your code never adds submodules from untrusted paths, this specific issue does not apply to you. Even so, upgrade to stay current with other fixes. Always treat any path you pass to a git library as sensitive when it comes from user input.
Why is writing a file the same as running code here?
Because the file lands in .git/hooks with a known hook name. Git runs hooks automatically on events such as checkout, commit and merge. A file named post-checkout, marked executable, becomes the script git runs after the next checkout. So dropping a file into that folder is enough to get commands run later, with no further action by the attacker.
Does catching the ValueError protect me on 3.2.0?
No. On 3.2.0 the ValueError is raised at index time, which is after the clone has already written files to disk. Catching the error only hides that the unsafe path was partly processed. The planted file is still there. The real protection is to upgrade to 3.2.1, where the path is rejected before any clone happens.
Why did git not just stop the clone into .git/hooks?
Git does refuse to clone into a folder that is not empty. That is why the attack needs an empty or missing .git/hooks folder to succeed. In many repositories the hooks folder holds sample files, which blocks the clone. Minimal, CI and container checkouts often have an empty hooks folder, so the slot is free and the planted hook can land.
Is there a CVE number for this GitPython vulnerability?
Yes, CVE-2026-114145, though not every page shows it yet. NVD published it on 21 October 2026. The GitHub Advisory Database and OSV list it as an alias of GHSA-83vg-56qc-22m7 from 22 October 2026. As of 28 October 2026, the GitPython repository's own advisory still reads no known CVE. Search by either id, since both name the same bug, rated High with a CVSS 3.1 score of 7.5.

Sources and further reading

Practise this with Lala

Practise