PYSEC-2026-3837
Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02Summary
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
- Entry: app calls
TagReference.create(repo, name, reference=<user>)withreference="--file=/home/app/.ssh/id_rsa". - Check:
Git.check_unsafe_options(_option_candidates([], kwargs), ["--file","-F"])@ tag.py:137-141. Guard: denylist includes--file/-F. Bypass proof:_option_candidatesreceivesargs=[]→ the positionalreferenceis never a candidate (the kwarg spellingfile="…"IS blocked; only the positional escapes). - Sink:
repo.git.tag(*args, **kwargs)@ tag.py:158 → no--. argv (observed):['git','tag','-f','vpwn','--file=<secret>']. - 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
| 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)"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.