{"id":"GHSA-w8jc-g24h-crhw","kind":"vulnerability","aliases":["GHSA-w8jc-g24h-crhw"],"related":[],"title":"GitPython command wrappers omit unsafe protocol and option checks","description":"### Summary\n\nGitPython has spent 2022-2026 rolling out two reusable guards method-by-method: `Git.check_unsafe_protocols(url)` (raises `UnsafeProtocolError` for `<transport>::` remote-helper URLs - CVE-2022-24439) and `Git.check_unsafe_options(...)` (raises `UnsafeOptionError` for denylisted option spellings - CVE-2023-40267, CVE-2026-42215, CVE-2026-67323). Three wrapper methods that build a `git` argv from caller-controlled values still have **neither**:\n\n1. **`Git.ls_remote`** (git/cmd.py:1128-1143) - checks options (:1139-1141) but forwards the URL positional unchecked; no `allow_unsafe_protocols` parameter exists. The same URL through `clone_from` raises `UnsafeProtocolError`.\n2. **`Repo.merge_base`** (git/repo/base.py:895-936) - forwards `**kwargs`/`*rev` to `self.git.merge_base` with no guard; guarded sibling `iter_commits` sits 12 lines up. Its exception handler additionally swallows every non-128 `GitCommandError` into `[]`, masking the injection.\n3. **`IndexFile.move`** (git/index/base.py:1092-1168) - forwards `**kwargs` to `git mv` twice (:1147, :1167); guarded sibling `remove()` is 23 lines above.\n\nVerified unfixed on `main @ a54d159` (2026-09-21), post-dating latest release 3.1.62 (2026-09-07) - all three sites re-verified today.\n\n### Impact\n\n**Site 1 (`ls_remote`)** - RCE conditional on the victim git transport policy. The common 'probe first, then clone' pattern (`Git(...).ls_remote(attacker_url)`) with an `ext::sh -c ...` URL executes arbitrary commands when the victim git permits the ext transport: dynamically verified with `protocol.ext.allow=always` (marker executed). git >= 2.38.1 stock config refuses the transport (`fatal: transport 'ext' not allowed`, rc 128) - GitPython adds no defense-in-depth and surfaces a bare `GitCommandError`. Environments on git < 2.38.1 are exposed with stock config - the exact population CVE-2022-24439 was scored against.\n\n**Sites 2/3** - latent argument-injection surface: caller kwargs flow 1:1 into the `git` argv before operands and `--` (cmd.py:1850 `opt_args + ext_args`), and `execute_kwargs` (incl. `shell=True`/`env` - the CVE-2023-40267 Windows mechanism) passes through unguarded. Today `git merge-base`/`git mv` expose no exec option, so we claim the missing-guard class (vendor-settled by CVE-2026-67323, which bundled archive/ls_remote/iter_commits/blame in one advisory), not direct RCE.\n\n### Dynamic verification (today; GitPython main @ a54d159, git 2.55.0, Linux)\n\n| # | Call | Result |\n|---|---|---|\n| T1 | `repo.git.ls_remote('ext::/tmp/gp_touch.sh')`, stock config | git refuses (rc 128); **no `UnsafeProtocolError` from GitPython** |\n| T1b | same + victim config `protocol.ext.allow=always` | **marker created - arbitrary command executed** |\n| T2 | `repo.git.ls_remote('.', upload_pack=...)` | blocked by cmd.py:1141 (options-side fixed - protocol-side is the residual gap) |\n| T3 | `repo.merge_base('main','side', upload_pack=...)` | **not blocked**; argv `git merge-base --upload-pack=... main side`; rc 129 silently converted to `[]` |\n| T4 | `repo.index.move(['f1','f2'], output=...)` | **not blocked**; option reaches `git mv` (rc 129) |\n\n### Coverage matrix (main @ a54d159)\n\n`check_unsafe_protocols`: 7 call sites (`_clone`, `archive`, `set_url`, `create`, `fetch`, `pull`, `push`) - none on `ls_remote`.\n`check_unsafe_options`: 24 call sites incl. `ls_remote`, `iter_commits`, `IndexFile.remove` - absent on `merge_base` and `move`.\n\n### Precedents (same repo, same guard family)\n\n- **CVE-2022-24439 / GHSA-hcpj-qp55-gfph** (critical): `check_unsafe_protocols` on the clone path - Site 1 is the same `ext::` primitive on a sibling API.\n- **CVE-2023-40267 / GHSA-pr76-5cm5-w9cj** (critical, 'incomplete fix for CVE-2022-24439'): `check_unsafe_options` - unguarded kwargs to argv treated as RCE (`shell=True` on Windows).\n- **CVE-2026-42215 / GHSA-rpm5-65cw-6hj4** (high, fixed 3.1.47): underscored-spelling bypass - rollout still active in 2026.\n- **CVE-2026-67323 / GHSA-956x-8gvw-wg5v** (high, 8.4, fixed 3.1.51): added `check_unsafe_options` to `archive`/`ls_remote`/`iter_commits`/`blame`; its PoC is literally `repo.git.ls_remote('.', upload_pack=...)`. Establishes that a wrapper missing the check is itself a vulnerability. This report is the residual gap of that rollout: `ls_remote` got options but not protocols; `merge_base`/`move` got neither.\n\n### Recommended fix (existing helpers, 1-3 lines per site)\n\n1. `Git.ls_remote`: `if not allow_unsafe_protocols: self.check_unsafe_protocols(args[0])` + new `allow_unsafe_protocols: bool = False` (the `_clone` pattern, base.py:1536-1538).\n2. `Repo.merge_base`: `check_unsafe_options(Git._option_candidates(list(rev), kwargs), unsafe_options=self.unsafe_git_revision_options)` (the `iter_commits` pattern); stop masking non-128 statuses.\n3. `IndexFile.move`: the `remove()` pattern with `unsafe_git_pathspec_from_file_options`.\n\nOne advisory, three affected methods, severity anchored on Site 1 - mirroring CVE-2026-67323's bundling.","summary":"GitPython's ls_remote, merge_base and IndexFile.move forward caller-supplied URLs and options straight to git without the library's safety guards. On systems where git allows the ext transport, a crafted URL passed to ls_remote can run arbitrary shell commands, and the other two methods let attacker-chosen options reach git.","severity":"high","cvss":7.5,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"GitPython","fixed":["3.2.0"],"affected":"<= 3.1.62","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-1287","git-tooling","gitpython","package","pypi","python","self-hosted"],"not_affected_if":["fixed-trusted-input","modern-git-default-config"],"how_to_check":"- Run `pip show GitPython` and check whether the installed version is `3.1.62` or older\n- Run `git --version`; git `2.38.1` or newer refuses the ext transport by default, so exposure is mainly on older git or where `protocol.ext.allow=always` is set","fix":"- Upgrade GitPython to `3.2.0`\n- Until upgraded, do not pass untrusted URLs or options to `Git.ls_remote`, `Repo.merge_base` or `IndexFile.move`","status":"open","published_at":"2026-10-27T15:26:43+00:00","first_seen_at":"2026-10-27T15:28:57+00:00","triaged_at":"2026-10-27T15:29:24+00:00","updated_at":"2026-10-27T15:29:24+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-27T15:29:24Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade GitPython to `3.2.0`\n- Until upgraded, do not pass untrusted URLs or options to `Git.ls_remote`, `Repo.merge_base` or `IndexFile.move`","reach":"internet","title":"GitPython: three command wrappers skip unsafe protocol and option checks","impact":"high","summary":"GitPython's ls_remote, merge_base and IndexFile.move forward caller-supplied URLs and options straight to git without the library's safety guards. On systems where git allows the ext transport, a crafted URL passed to ls_remote can run arbitrary shell commands, and the other two methods let attacker-chosen options reach git.","category":"package","nicknames":[],"who_cares":["python","pypi","gitpython","self-hosted","git-tooling"],"confidence":0.8,"needs_auth":"no","worst_case":"code runs on the server via ext transport URL","analyst_note":"Confirm whether the advisory's fixed version 3.2.0 is the only fix and whether any git version or config other than protocol.ext.allow=always enables the ext transport.","how_to_check":"- Run `pip show GitPython` and check whether the installed version is `3.1.62` or older\n- Run `git --version`; git `2.38.1` or newer refuses the ext transport by default, so exposure is mainly on older git or where `protocol.ext.allow=always` is set","needs_analyst":true,"not_affected_if":["fixed-trusted-input","modern-git-default-config"],"who_is_affected":"Applications that call GitPython's ls_remote with a URL an outside user can influence, and that run on git versions older than 2.38.1 or with the ext transport allowed, are at risk of command execution. Code that only passes fixed, trusted URLs and options, or that only uses merge_base or move with internal values, has a much smaller exposure. Teams on managed hosting with modern git defaults face mainly argument injection.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.000898,"input_tokens":5496,"output_tokens":696,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-07':255C '-09':245C,254C '-1141':162C '-1143':158C '-1168':223C '-128':213C '-1287':22B '-2022':116C,351C,571C,610C '-2023':129C,389C,598C '-2026':94C,132C,135C,420C,627C,647C '-21':246C '-24439':117C,352C,572C,611C '-40267':130C,390C,599C '-42215':133C,628C '-67323':136C,421C,648C '-936':188C '/tmp/gp_touch.sh':446C '1':153C,265C,366C,367C,587C '1092':222C '1128':157C '1139':161C '1147':230C '1167':231C '12':203C '128':325C,452C '129':507C,523C '2':183C '2.38.1':313C,341C '2.55.0':438C '2/3':357C '2022':93C '2026':244C,253C,645C '23':236C '24':549C '3':219C '3.1.47':636C '3.1.62':252C '5cm5':603C '65cw':632C '6hj4':633C '7':531C '895':187C '8gvw':652C '956x':651C 'a54d159':243C,436C,527C 'absent':558C 'activ':643C 'add':327C 'addit':209C 'advisori':427C 'allow':61B,170C,323C 'alway':309C,462C 'api':596C 'arbitrari':74B,295C,465C 'archiv':535C 'archive/ls_remote/iter_commits/blame':424C 'arg':377C,379C 'argument':360C 'argument-inject':359C 'argv':144C,371C,496C,618C 'attack':84B,286C 'attacker-chosen':83B 'bare':336C 'base':38B,185C,194C,400C,489C,500C,561C 'block':473C,495C,517C 'build':141C 'bundl':423C 'bypass':640C 'c':292C 'call':440C,532C,550C 'caller':43B,147C,363C 'caller-control':146C 'caller-suppli':42B 'check':9A,159C,528C,546C,579C,612C 'chosen':85B 'claim':409C 'class':414C 'clone':179C,281C,534C,584C 'cmd.py:1141':475C 'cmd.py:1850':375C 'command':2A,76B,296C,466C 'commit':201C,556C 'common':277C 'condit':269C 'config':315C,346C,448C,460C 'control':148C 'convert':509C 'coverag':524C 'craft':66B 'creat':464C,538C 'crhw':14A,19A 'critic':578C,605C 'cve':115C,128C,131C,134C,350C,388C,419C,570C,597C,609C,626C,646C 'cwe':21B 'date':249C 'defens':330C 'defense-in-depth':329C 'denylist':125C 'depth':332C 'direct':429C 'dynam':305C,431C 'env':386C 'environ':338C 'everi':211C 'exact':348C 'except':207C 'exec':405C 'execut':294C,311C,381C,467C 'exist':174C 'expos':343C,403C 'ext':63B,290C,303C,321C,378C,445C,591C 'f1':513C 'f2':514C 'famili':569C 'fatal':319C 'fetch':539C 'first':279C 'fix':479C,607C,635C 'flow':365C 'forward':41B,164C,189C,224C 'g24h':13A,18A 'gap':486C 'gfph':577C 'ghsa':11A,16A,574C,601C,630C,650C 'ghsa-956x-8gvw-wg5v':649C 'ghsa-hcpj-qp55-gfph':573C 'ghsa-pr76-5cm5-w9cj':600C 'ghsa-rpm5-65cw-6hj4':629C 'ghsa-w8jc-g24h-crhw':10A,15A 'git':24B,50B,60B,88B,143C,227C,273C,283C,300C,312C,340C,370C,397C,401C,437C,449C,497C,520C 'git-tool':23B 'git.check':104C,119C 'git.ls':154C 'git/cmd.py':156C 'git/index/base.py':221C 'git/repo/base.py':186C 'gitcommanderror':214C,337C 'gitpython':1A,20A,26B,33B,90C,326C,434C,456C 'guard':56B,99C,197C,198C,232C,413C,568C 'handler':208C 'hcpj':575C 'helper':113C 'high':634C 'host':32B 'impact':263C 'incl':383C,552C 'incomplet':606C 'indexfile.move':40B,220C 'indexfile.remove':557C 'inject':218C,361C 'iter':200C,555C 'kwarg':190C,225C,364C,382C,616C 'latent':358C 'latest':250C 'let':82B 'librari':53B 'line':204C,237C 'linux':439C 'ls':35B,70B,266C,284C,544C,553C 'main':242C,435C,490C,504C,526C 'marker':310C,463C 'mask':216C 'matrix':525C 'mechan':392C 'merg':37B,399C,499C,560C 'merge-bas':398C,498C 'method':81B,101C,103C,139C 'method-by-method':100C 'miss':412C 'missing-guard':411C 'move':563C 'mv':228C,402C,521C 'neither':152C 'non':212C 'none':542C 'omit':4A 'one':426C 'operand':373C 'opt':376C 'option':8A,47B,86B,121C,126C,160C,406C,477C,518C,548C,614C 'options-sid':476C 'output':515C 'pack':472C,493C,503C 'packag':27B 'paramet':173C 'pass':68B,393C 'path':585C 'pattern':282C 'permit':301C 'polici':275C 'popul':349C 'posit':167C 'post':248C 'post-dat':247C 'pr76':602C 'preced':564C 'primit':592C 'probe':278C 'protocol':6A,106C,172C,481C,530C,581C 'protocol-sid':480C 'protocol.ext.allow':308C,461C 'pull':540C 'push':541C 'pypi':28B 'python':29B 'qp55':576C 'rais':108C,122C,181C 'rc':324C,451C,506C,522C 'rce':268C,430C,621C 're':260C 're-verifi':259C 'reach':87B,519C 'refus':316C,450C 'releas':251C 'remot':36B,71B,112C,155C,267C,285C,444C,470C,545C,554C 'remote-help':111C 'remov':234C 'repo':566C 'repo.git.ls':443C,469C 'repo.index.move':512C 'repo.merge':184C,488C 'residu':485C 'result':441C 'reusabl':98C 'rev':191C 'roll':95C 'rollout':641C 'rpm5':631C 'run':73B 'safeti':55B 'score':354C 'self':31B 'self-host':30B 'self.git.merge':193C 'set':536C 'settl':417C 'sh':291C 'shell':75B,384C,622C 'sibl':199C,233C,595C 'side':478C,482C,491C,505C 'silent':508C 'sit':202C 'site':258C,264C,356C,533C,551C,586C 'spell':127C,639C 'spent':92C 'still':150C,642C 'stock':314C,345C,447C 'straight':48B 'summari':89C 'suppli':44B 'surfac':334C,362C 'swallow':210C 'system':58B 't1':442C 't1b':457C 't2':468C 't3':487C 't4':511C 'three':137C,257C 'today':262C,396C,433C 'tool':25B 'transport':64B,274C,304C,318C,320C 'treat':619C 'true':385C,623C 'twice':229C 'two':80B,97C 'uncheck':168C 'underscor':638C 'underscored-spel':637C 'unfix':240C 'unguard':395C,615C 'unsaf':5A,105C,120C,171C,529C,547C,580C,613C 'unsafeoptionerror':123C 'unsafeprotocolerror':109C,182C,454C 'upload':471C,492C,502C 'upload-pack':501C 'url':45B,67B,107C,114C,166C,177C,287C,293C,537C 'valu':149C 'vendor':416C 'vendor-settl':415C 'verif':432C 'verifi':239C,261C,306C 'victim':272C,299C,459C 'w8jc':12A,17A 'w9cj':604C 'wg5v':653C 'window':391C,625C 'without':51B 'wrapper':3A,138C","nicknames":[],"mitigations":[],"who_affected":"Applications that call GitPython's ls_remote with a URL an outside user can influence, and that run on git versions older than 2.38.1 or with the ext transport allowed, are at risk of command execution. Code that only passes fixed, trusted URLs and options, or that only uses merge_base or move with internal values, has a much smaller exposure. Teams on managed hosting with modern git defaults face mainly argument injection.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-27T15:26:43+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-w8jc-g24h-crhw","url":"https://github.com/advisories/GHSA-w8jc-g24h-crhw","severity":"high","published_at":"2026-10-27T15:26:43+00:00","modified_at":"2026-10-27T15:26:45+00:00","first_seen_at":"2026-10-27T15:28:57+00:00","fetched_at":"2026-10-27T15:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}