{"id":"PYSEC-2026-3837","summary":"GitPython: TagReference.create positional reference bypasses kwargs-only --file guard, enabling arbitrary file read (incomplete fix of 3af0c251)","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'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).\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` — 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's 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 — 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=[]` → the positional `reference` is never a candidate (the kwarg spelling `file=\"…\"` IS blocked; only the positional escapes).\n3. Sink: `repo.git.tag(*args, **kwargs)` @ tag.py:158 → no `--`. argv (observed): `['git','tag','-f','vpwn','--file=\u003csecret\u003e']`.\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,'vp','--file=\u003csecret\u003e')` → PASSED; `tag.message == 'GATE_SECRET_LINE_A\\nGATE_SECRET_LINE_B'`. Control: `TagReference.create(..., file='\u003csecret\u003e')` → `UnsafeOptionError: --file is not allowed`. Fix-commit read: `3af0c251` adds `_option_candidates([], kwargs)` (empty args → 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)** — GitHub: @manus-use","aliases":["CVE-2026-78679","GHSA-3wxw-xv34-2frg"],"modified":"2026-09-10T12:15:03.987482723Z","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"}],"affected":[{"package":{"name":"gitpython","ecosystem":"PyPI","purl":"pkg:pypi/gitpython"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.1.59"}]}],"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"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/gitpython/PYSEC-2026-3837.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"}]}