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

PYSEC-2026-3837

Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02
VLAI
Details

Summary

TagReference.create() forwards a caller-influenced positional reference value into git tag without it ever being inspected by the unsafe-option guard, allowing an arbitrary file read (the file's contents are returned in-band as the annotated tag message). This is an incomplete-fix bypass of commit 3af0c251 (the fix for GHSA-3f7w-8rr8-f37f's tag instance).

Root Cause

The fix 3af0c251 added unsafe_git_tag_options = ["--file","-F"] and a guard call, but the guard is Git.check_unsafe_options(options=Git._option_candidates([], kwargs), unsafe_options=...) at git/refs/tag.py:139 — it passes an EMPTY args list and inspects kwargs only. The dangerous values path and reference are POSITIONALS (args = (path, reference), tag.py:156), placed before any --. A user-influenced reference="--file=<path>" therefore reaches git tag as the exact --file option the fix intended to block, creating an annotated tag whose message is the file's contents.

Impact

Arbitrary local file read at the privileges of the host process; contents returned in-band via tagref.tag.message. Requires the embedding application to forward a caller-influenced reference value into TagReference.create() (pure VALUE control — the CVE-2026-42215 threat model). Default allow_unsafe_options=False.

Proof of Concept

from git import TagReference
t = TagReference.create(repo, "vpwn", reference="--file=/home/app/.ssh/id_rsa")
print(t.tag.message)   # contents of the file

Attack Chain

  1. Entry: app calls TagReference.create(repo, name, reference=<user>) with reference="--file=/home/app/.ssh/id_rsa".
  2. Check: Git.check_unsafe_options(_option_candidates([], kwargs), ["--file","-F"]) @ tag.py:137-141. Guard: denylist includes --file/-F. Bypass proof: _option_candidates receives args=[] → the positional reference is never a candidate (the kwarg spelling file="…" IS blocked; only the positional escapes).
  3. Sink: repo.git.tag(*args, **kwargs) @ tag.py:158 → no --. argv (observed): ['git','tag','-f','vpwn','--file=<secret>'].
  4. Impact: annotated tag created; tagref.tag.message == file contents (arbitrary file read).

Bypass Evidence

Independently reproduced (independent test harness, git 2.43.0, default allow_unsafe_options=False): TagReference.create(repo,'vp','--file=<secret>') → PASSED; tag.message == 'GATE_SECRET_LINE_A\nGATE_SECRET_LINE_B'. Control: TagReference.create(..., file='<secret>')UnsafeOptionError: --file is not allowed. Fix-commit read: 3af0c251 adds _option_candidates([], kwargs) (empty args → positional never a candidate).

Affected Versions

GitPython <= 3.1.58 (sink present verbatim on the latest release tag; git diff 3.1.57..HEAD touches only test files).

Suggested Fix

Include the positional reference (and path) in the option-candidate list passed to check_unsafe_options, or place a -- separator before the positional arguments in TagReference.create().


Reported by zx (Jace) — GitHub: @manus-use

Impacted products
Name purl
gitpython pkg:pypi/gitpython

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "gitpython",
        "purl": "pkg:pypi/gitpython"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.1.59"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "0.1.7",
        "0.2.0-beta1",
        "0.3.0-beta1",
        "0.3.0-beta2",
        "0.3.1-beta2",
        "0.3.2",
        "0.3.2.1",
        "0.3.2.RC1",
        "0.3.3",
        "0.3.4",
        "0.3.5",
        "0.3.6",
        "0.3.7",
        "1.0.0",
        "1.0.1",
        "1.0.2",
        "2.0.0",
        "2.0.1",
        "2.0.2",
        "2.0.3",
        "2.0.4",
        "2.0.5",
        "2.0.6",
        "2.0.7",
        "2.0.8",
        "2.0.9",
        "2.0.9.dev0",
        "2.0.9.dev1",
        "2.1.0",
        "2.1.1",
        "2.1.10",
        "2.1.11",
        "2.1.12",
        "2.1.13",
        "2.1.14",
        "2.1.15",
        "2.1.2",
        "2.1.3",
        "2.1.4",
        "2.1.5",
        "2.1.6",
        "2.1.7",
        "2.1.8",
        "2.1.9",
        "3.0.0",
        "3.0.1",
        "3.0.2",
        "3.0.3",
        "3.0.4",
        "3.0.5",
        "3.0.6",
        "3.0.7",
        "3.0.8",
        "3.0.9",
        "3.1.0",
        "3.1.1",
        "3.1.10",
        "3.1.11",
        "3.1.12",
        "3.1.13",
        "3.1.14",
        "3.1.15",
        "3.1.16",
        "3.1.17",
        "3.1.18",
        "3.1.19",
        "3.1.2",
        "3.1.20",
        "3.1.22",
        "3.1.23",
        "3.1.24",
        "3.1.25",
        "3.1.26",
        "3.1.27",
        "3.1.28",
        "3.1.29",
        "3.1.3",
        "3.1.30",
        "3.1.31",
        "3.1.32",
        "3.1.33",
        "3.1.34",
        "3.1.35",
        "3.1.36",
        "3.1.37",
        "3.1.38",
        "3.1.4",
        "3.1.40",
        "3.1.41",
        "3.1.42",
        "3.1.43",
        "3.1.44",
        "3.1.45",
        "3.1.46",
        "3.1.47",
        "3.1.48",
        "3.1.49",
        "3.1.5",
        "3.1.50",
        "3.1.51",
        "3.1.52",
        "3.1.53",
        "3.1.54",
        "3.1.55",
        "3.1.56",
        "3.1.57",
        "3.1.58",
        "3.1.6",
        "3.1.7",
        "3.1.8",
        "3.1.9"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-78679",
    "GHSA-3wxw-xv34-2frg"
  ],
  "details": "## Summary\n`TagReference.create()` forwards a caller-influenced positional `reference` value into `git tag` without it ever being inspected by the unsafe-option guard, allowing an arbitrary file read (the file\u0027s contents are returned in-band as the annotated tag message). This is an incomplete-fix bypass of commit `3af0c251` (the fix for GHSA-3f7w-8rr8-f37f\u0027s tag instance).\n\n## Root Cause\nThe fix `3af0c251` added `unsafe_git_tag_options = [\"--file\",\"-F\"]` and a guard call, but the guard is `Git.check_unsafe_options(options=Git._option_candidates([], kwargs), unsafe_options=...)` at `git/refs/tag.py:139` \u2014 it passes an EMPTY args list and inspects **kwargs only**. The dangerous values `path` and `reference` are POSITIONALS (`args = (path, reference)`, tag.py:156), placed before any `--`. A user-influenced `reference=\"--file=\u003cpath\u003e\"` therefore reaches `git tag` as the exact `--file` option the fix intended to block, creating an annotated tag whose message is the file\u0027s contents.\n\n## Impact\nArbitrary local file read at the privileges of the host process; contents returned in-band via `tagref.tag.message`. Requires the embedding application to forward a caller-influenced `reference` value into `TagReference.create()` (pure VALUE control \u2014 the CVE-2026-42215 threat model). Default `allow_unsafe_options=False`.\n\n## Proof of Concept\n```python\nfrom git import TagReference\nt = TagReference.create(repo, \"vpwn\", reference=\"--file=/home/app/.ssh/id_rsa\")\nprint(t.tag.message)   # contents of the file\n```\n\n## Attack Chain\n1. Entry: app calls `TagReference.create(repo, name, reference=\u003cuser\u003e)` with `reference=\"--file=/home/app/.ssh/id_rsa\"`.\n2. Check: `Git.check_unsafe_options(_option_candidates([], kwargs), [\"--file\",\"-F\"])` @ tag.py:137-141. Guard: denylist includes `--file`/`-F`. Bypass proof: `_option_candidates` receives `args=[]` \u2192 the positional `reference` is never a candidate (the kwarg spelling `file=\"\u2026\"` IS blocked; only the positional escapes).\n3. Sink: `repo.git.tag(*args, **kwargs)` @ tag.py:158 \u2192 no `--`. argv (observed): `[\u0027git\u0027,\u0027tag\u0027,\u0027-f\u0027,\u0027vpwn\u0027,\u0027--file=\u003csecret\u003e\u0027]`.\n4. Impact: annotated tag created; `tagref.tag.message` == file contents (arbitrary file read).\n\n## Bypass Evidence\nIndependently reproduced (independent test harness, git 2.43.0, default `allow_unsafe_options=False`): `TagReference.create(repo,\u0027vp\u0027,\u0027--file=\u003csecret\u003e\u0027)` \u2192 PASSED; `tag.message == \u0027GATE_SECRET_LINE_A\\nGATE_SECRET_LINE_B\u0027`. Control: `TagReference.create(..., file=\u0027\u003csecret\u003e\u0027)` \u2192 `UnsafeOptionError: --file is not allowed`. Fix-commit read: `3af0c251` adds `_option_candidates([], kwargs)` (empty args \u2192 positional never a candidate).\n\n## Affected Versions\n`GitPython \u003c= 3.1.58` (sink present verbatim on the latest release tag; `git diff 3.1.57..HEAD` touches only test files).\n\n## Suggested Fix\nInclude the positional `reference` (and `path`) in the option-candidate list passed to `check_unsafe_options`, or place a `--` separator before the positional arguments in `TagReference.create()`.\n\n---\nReported by **zx (Jace)** \u2014 GitHub: @manus-use",
  "id": "PYSEC-2026-3837",
  "modified": "2026-09-10T11:02:07.125202Z",
  "published": "2026-09-10T09:45:00.699290Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-3wxw-xv34-2frg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-78679"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/pull/2208"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/commit/1b0d2d9b91575f7db44ef4ff58ac37fc9335e5f6"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/gitpython-developers/GitPython"
    },
    {
      "type": "WEB",
      "url": "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.59"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/gitpython-before-arbitrary-file-read-via-tagreference-create"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/gitpython"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-3wxw-xv34-2frg"
    }
  ],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "GitPython: TagReference.create positional reference bypasses kwargs-only --file guard, enabling arbitrary file read (incomplete fix of 3af0c251)"
}



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…

Loading…