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
- Entry: app calls
TagReference.create(repo, name, reference=<user>) with reference="--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_candidates receives args=[] → the positional reference is never a candidate (the kwarg spelling file="…" 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
Summary
TagReference.create()forwards a caller-influenced positionalreferencevalue intogit tagwithout 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 commit3af0c251(the fix for GHSA-3f7w-8rr8-f37f's tag instance).Root Cause
The fix
3af0c251addedunsafe_git_tag_options = ["--file","-F"]and a guard call, but the guard isGit.check_unsafe_options(options=Git._option_candidates([], kwargs), unsafe_options=...)atgit/refs/tag.py:139— it passes an EMPTY args list and inspects kwargs only. The dangerous valuespathandreferenceare POSITIONALS (args = (path, reference), tag.py:156), placed before any--. A user-influencedreference="--file=<path>"therefore reachesgit tagas the exact--fileoption 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-influencedreferencevalue intoTagReference.create()(pure VALUE control — the CVE-2026-42215 threat model). Defaultallow_unsafe_options=False.Proof of Concept
Attack Chain
TagReference.create(repo, name, reference=<user>)withreference="--file=/home/app/.ssh/id_rsa".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).repo.git.tag(*args, **kwargs)@ tag.py:158 → no--. argv (observed):['git','tag','-f','vpwn','--file=<secret>'].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:3af0c251adds_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..HEADtouches only test files).Suggested Fix
Include the positional
reference(andpath) in the option-candidate list passed tocheck_unsafe_options, or place a--separator before the positional arguments inTagReference.create().Reported by zx (Jace) — GitHub: @manus-use