KEVWire

Security intelligence, with context.

Response snapshot Oct 27, 2026, 16:01 UTC

Back to the wire
Permalink
VulnerabilityGHSA-gq48-pqfc-9p58

GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree

GitPython's public `Submodule.move()` API verifies that its destination is lexically inside the parent repository, but it does not reject symbolic links in the destination's intermediate components before filesystem operations.

At a glance

Exploited
Not known
Severity
Not scored yet
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: GitPython: 3.2.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-gq48-pqfc-9p58 (opens a new tab)

What happened

## Summary

GitPython's public `Submodule.move()` API verifies that its destination is lexically inside the parent repository, but it does not reject symbolic links in the destination's intermediate components before filesystem operations. On systems that check out Git symlinks, an untrusted repository can therefore redirect a fixed, application-selected move destination outside the parent worktree.

The reporter reproduced this on the latest release, GitPython 3.1.62, and current `main`. In the strict scenario, the repository commits `vendor -> ../outside`, while the consuming application independently passes the fixed literal `vendor/module`. `Submodule.move()` follows `vendor`, moves the initialized submodule checkout to `outside/module`, rewrites `.gitmodules` and the index to the symlinked path, and leaves the index unable to produce a tree.

The same result occurs when the committed symlink contains an absolute path to an external directory. No attacker-controlled API argument, local symlink planting, race, Git hook, unsafe-option flag, or network access during the proof is required.

Continue reading

## Trust boundary and preconditions

The attacker controls:

- an untrusted Git repository that the application clones; - a mode-`120000` tracked symlink named `vendor`; - the initialized submodule's content.

The consuming application controls the call and uses a fixed layout rule:

```python submodule.move("vendor/module") ```

The trigger requires the application to initialize the submodule and later call `Submodule.move()` with a path below the attacker-committed symlink. Ordinary clone or submodule update does not automatically make this call. Windows was not tested. The primitive applies where Git materializes the tracked entry as a real symlink; on Windows that generally requires `core.

symlinks=true` with Developer Mode or sufficient privilege. Configurations that materialize it as an ordinary file do not take this path.

## Product and directly verified revisions

- Package: `GitPython` (`pip` ecosystem) - Latest release: 3.1.62, tag commit `db47516dac750c5968b8f6d388b18712c355df50` - Current `main`: `b62e91be897f687a622e40cd1e59c33dce45f124` - Relevant `git/objects/submodule/base.py` SHA-256 in both revisions: `a4355d49e0b75c7c7e74d33ac70ff9550f940c977848c99da40eb41e034b189c` - Upstream state rechecked: 2026-09-10 KST

Only these two revisions were directly tested.

## Severity and weakness

The demonstrated impact is an integrity boundary violation: repository content selects an existing directory outside the worktree, and GitPython moves an attacker-influenced checkout into a missing or empty child path there with the victim process's permissions. It also removes the original checkout and corrupts the parent repository's index/configuration state.

For a downstream service that remotely accepts repositories and performs this exact layout operation with user involvement retained, `AV:N` gives a conditional application-level score of 7.1. A fully automatic service may warrant a different `UI` assessment. The library report uses `AV:L` because the vulnerable API call is a separate application action and is not part of GitPython's ordinary clone/update flow.

No confidentiality impact, code execution, overwrite of an existing file, or overwrite of a non-empty directory was demonstrated.

## Reproducer

The following script creates only temporary local repositories and a self-generated canary. It passes `protocol.file.allow=always` separately with `-c` on each fixture Git invocation; that permission is process-scoped and is not persisted to global or repository configuration.

Run it from a fresh source checkout:

```bash git clone --depth 1 --branch 3.1.62 https://github.com/gitpython-developers/GitPython.git python3 poc.py /path/to/GitPython python3 poc.py --absolute-link /path/to/GitPython ```

`--absolute-link` commits the temporary external directory's absolute path instead of `../outside`.

```python #!/usr/bin/env python3 """Local-only reproducer for GitPython Submodule.move() symlink traversal."""

from __future__ import annotations

import argparse import json import subprocess import sys import tempfile from pathlib import Path from typing import Optional

def run_git( *args: str, cwd: Optional[Path] = None, check: bool = True ) -> subprocess.CompletedProcess: command = [ "git", "-c", "user.name=GitPython security fixture", "-c", "user.email=fixture@example.invalid", "-c", "protocol.file.allow=always", *args, ] return subprocess.run(command, cwd=cwd, check=check, text=True, capture_output=True)

def read_core_worktree(parent) -> str: module_config = Path(parent.git_dir) / "modules" / "logical-name" / "config" return parent.git.config("--file", str(module_config), "--get", "core.worktree")

def read_gitmodules_path(parent) -> str: modules_file = Path(parent.working_tree_dir) / ".gitmodules" return parent.git.config( "--file", str(modules_file), "--get", "submodule.logical-name.path" )

def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("source_root", type=Path, help="GitPython source checkout") parser.add_argument( "--absolute-link", action="store_true", help=( "Use the temporary external directory's absolute path as the committed " "symlink target" ), ) args = parser.parse_args()

source_root = args.source_root.resolve() # The tested GitPython source trees include gitdb and smmap at their roots. sys.path[:0] = [ str(source_root), str(source_root / "gitdb"), str(source_root / "smmap"), ] import gitdb # pylint: disable=import-outside-toplevel import smmap # pylint: disable=import-outside-toplevel from git import Repo # pylint: disable=import-outside-toplevel

with tempfile.TemporaryDirectory(prefix="gitpython-move-poc-") as temporary: root = Path(temporary) module_source = root / "module-source" attacker = root / "attacker-parent" victim = root / "victim-parent" outside = root / "outside"

run_git("init", "--quiet", str

Affected package ranges reported by GitHub: GitPython: < 3.2.0

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-gq48-pqfc-9p58 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-gq48-pqfc-9p58, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 27). GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree. https://kevwire.com/record/GHSA-gq48-pqfc-9p58

MLA

"GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree." KEVWire, 27 Oct. 2026, kevwire.com/record/GHSA-gq48-pqfc-9p58.

Chicago

KEVWire. "GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree." Last modified October 27, 2026. https://kevwire.com/record/GHSA-gq48-pqfc-9p58.

Evidence summary (plain text)

GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree (Oct 27, 2026).
GitPython's public `Submodule.move()` API verifies that its destination is lexically inside the parent repository, but it does not reject symbolic links in the destination's intermediate components before filesystem operations. On systems that check out Git symlinks, an untrusted repository can therefore redirect a fixed, application-selected move destination outside the parent worktree.

The reporter reproduced this on the latest release, GitPython 3.1.62, and current `main`. In the strict scenario, the repository commits `vendor -> ../outside`, while the consuming application independently passes the fixed literal `vendor/module`. `Submodule.move()` follows `vendor`, moves the initialized submodule checkout to `outside/module`, rewrites `.gitmodules` and the index to the symlinked path, and leaves the index unable to produce a tree.
Sources:
GitHub-reviewed advisory GHSA-gq48-pqfc-9p58: https://github.com/advisories/GHSA-gq48-pqfc-9p58
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: GHSA-gq48-pqfc-9p58.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 27, 2026, 16:00 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are not available yet. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail