- Git の
--は一般的なオプション終端子ではなく、リビジョンとパス指定を区別するためのものなので、信頼できないリビジョンを安全に渡すには Git 2.24.0 からサポートされた--end-of-optionsが必要 git log --end-of-options "$rev" -- "$path"では、前の記号は オプションとリビジョン、後ろの--はリビジョンとパスを区別しており、相互に置き換えられない- シェルを介さずに
argv配列を直接実行しても、ダッシュで始まる入力が--upload-pack、core.sshCommand、ProxyCommandのようなオプションとして解釈されると、CWE-88 引数インジェクションが発生しうる - 調査した 19 個のパッケージマネージャのうち 17 個がデフォルトまたは唯一の方法として Git バイナリを実行していたが、
--end-of-optionsを使っていたツールは Go のcmd/goだけだった - 根本的な対策には、サブコマンドに応じて Git の最小バージョンを 2.24.0・2.30.0・2.43.1 に引き上げる互換性コストがあり、Git ライブラリは引数インジェクションの境界をなくす代わりに upstream のチェックアウト安全性修正を自前で追跡する必要がある
-- と --end-of-options の違い
- 一般的な Unix ツールでは
--はオプション解釈の終わりを示すため、rm -- -fは-fを強制削除オプションではなくファイル名として扱う - Git は早い時期から
--を リビジョンとパス指定(pathspec)の区切り文字として使ってきたgit log fooはfooというブランチとファイルのどちらを意味するのか曖昧git log main -- README.mdは、mainのコミットのうちREADME.mdを変更したコミットを意味する
- この設計のため、リビジョン位置にはオプション終端の記号がなく、
git log "$rev"で$revがダッシュで始まると Git はそれをオプションとして解釈する --end-of-optionsを導入したコミットは、既存の--がすでにリビジョンとパス指定を分けているため、オプションとリビジョンを区別する別の記号が必要だと説明している--end-of-optionsは gitcli(7) に文書化されており、Git 2.24.0 がリリースされた 2019 年 11 月に追加された
コマンドごとの正しい使い方
git clone -- "$url"は、cloneが POSIX の慣例に従うため、URL の前の--がオプション解釈を終了させるgit checkout "$ref" --の後ろ側の--は$refをファイル名ではなくリビジョンとして示すが、$refが先にオプションとして解釈されることまでは防げない- 信頼できないリビジョンとパスを一緒に安全に渡すには、
git log --end-of-options "$rev" -- "$path"のように 2 つの記号を両方使う必要がある--end-of-optionsはオプションとリビジョンを区別する--はリビジョンとパスを分ける
- 2 つの記号を相互に置き換え可能なものとして扱うと、ダッシュで始まる入力を防げない
サブコマンドごとに異なるサポート時期
--end-of-optionsのサポートはすべての Git コマンドに一度に適用されたわけではなく、サブコマンドごとに追加されたgit rev-parseは独自の引数パーサを使っているため、最初の導入から 1 年遅れて Git 2.30.0 でサポート開始となったgit checkoutとgit resetは独自に--を解釈しており、初期実装では--end-of-optionsを引数リストに残してしまうため、これを拒否していた- この問題は Git 2.43.1 がリリースされた 2024 年 2 月に解決された
シェルなしでも起こる引数インジェクション
- Git、Mercurial、SSH には、呼び出し元が指定したコマンドを実行するオプションが正式機能として存在する
git clone --upload-pack=<cmd>はサーバー側バイナリを指定する- すべての Git 呼び出しにおける
-c core.sshCommand=<cmd>は接続コマンドを変更する - Mercurial の
--config=alias.<subcmd>=!<shell>は実行するサブコマンドを任意のシェルスクリプトで再定義する - SSH の
-oProxyCommand=<cmd>はプロキシコマンドを指定する
- ラッパープログラムが信頼できない文字列を引数リストに入れると、これらの機能は 攻撃手段に変わりうる
- この失敗タイプは CWE-88 の 引数インジェクション(argument injection) に該当し、シェルコマンドインジェクションとは異なる
- プログラムが
system()の代わりにargv配列とexecを使っていても発生する - 配列は破損せずに Git に渡されるが、Git がダッシュで始まる引数をオプションとして解釈してしまう
- プログラムが
docker buildの CVE-2019-13139 は Go のos/execとargv配列を使い、シェルを経由しない事例だった- Git コンテキスト URL の
#ref:dirフラグメントがgit fetch origin <ref>に渡され、その<ref>が--upload-pack=<cmd>として解釈された
- Git コンテキスト URL の
複数のバージョン管理システムで繰り返された脆弱性
- 2017 年 8 月の同じ日に、4 つのバージョン管理システムで同一パターンが公開された
- Git の CVE-2017-1000117
- Mercurial の CVE-2017-1000116
- Subversion の CVE-2017-9800
- CVS の CVE-2017-12836
- 4 つのシステムはいずれも URL のホスト名を SSH 引数として渡しており、
-oProxyCommand=で始まるホスト名が SSH オプションとして処理された - Phabricator の事後分析によると、当時活発に保守されていた 3 つのツールのうち、Subversion だけがホスト名の前に
--を追加していた- Git と Mercurial は、すべての SSH 実装が
--をサポートするわけではないという理由もあり、ホスト名形式を検証していた
- Git と Mercurial は、すべての SSH 実装が
--が欠けたコードでも、引数がダッシュで始まるまでは正常に見えて動作するため、この仕組みは 本質的に安全ではない
パッケージマネージャが露出する経路
- パッケージマネージャはマニフェスト、ロックファイル、推移的依存関係メタデータから Git URL や ref を受け取り、サブプロセスに渡す
- Gemfile の
gem 'foo', git: '...' package.jsonのgithub:user/repo#refpyproject.toml、Cargo.toml、mix.exs、Package.swift、pubspec.yaml、conanfile.py、go.modの同等設定
- Gemfile の
- 2026 年 7 月時点の HEAD を基準に調査した 19 個のパッケージマネージャのうち 17 個が、デフォルトまたは唯一の経路として Git バイナリを実行していた
- 残る 2 つはライブラリをデフォルトで使っている
- Cargo は libgit2 を使用し、
net.git-fetch-with-cliを有効にすると Git プロセスを実行する - Poetry は 1.2.0 から dulwich に移行しており、
system-git-client設定でシステム Git を使える
- Cargo は libgit2 を使用し、
- Nix はローカルリポジトリを読むときは libgit2 を使うが、libgit2 が git-credential ヘルパーをサポートしていないため、fetch では Git プロセスを実行する
- 調査対象は Bundler、Cargo、CocoaPods、Composer、Conan、Go、Helm、Homebrew、Mix、Nix、npm、pip、pnpm、Poetry、Pub、SwiftPM、uv、vcpkg、Yarn
パッケージマネージャで確認された CVE
- この種の公開済みパッケージマネージャ脆弱性には、次の事例がある
- Bundler の CVE-2021-43809
- Composer の CVE-2021-29472、CVE-2022-24828
- Poetry の CVE-2022-36069
- pip の CVE-2023-5752
- CocoaPods の CVE-2022-21223、CVE-2022-24440
- Go の CVE-2025-68119
- 2022 年の複数の脆弱性を発見した Snyk は、Git と Mercurial の引数インジェクション研究を公開した
- Sonar はバイナリごとの 危険なオプション一覧を管理している
実際の防御状況と Go の修正
- Git プロセスを実行する 17 個のパッケージマネージャのうち、
--end-of-optionsを使っていたツールは Go のcmd/goだけだった - Go は 2019 年 6 月、一般的な防御強化の一環として リポジトリ URL の前に
--を追加した - 2026 年 1 月になって
--だけでは不十分だと判明し、CVE-2025-68119 の修正として--end-of-optionsを全体的に追加した - 同じ修正には
HGPLAIN=+strictflagsも含まれている- この設定は Mercurial 4.4.2 がリリースされた 2017 年から、Mercurial の初期オプション解釈を制限している
- Go の修正コミットは、同じ問題が再導入されにくいよう、より構造的な変更が必要かもしれないとしつつも、まず現状の問題を解決している
防御の大半は脆弱性公開後に追加
- 残りのパッケージマネージャは、引数リストを保護するとしても、主に
--または入力の 先頭ダッシュ拒否を使っている - Bundler の
git cloneURL 前の--は、CVE-2021-43809 のパッチで追加された - cocoapods-downloader の先頭ダッシュ拒否は、CVE-2022-21223 の公開時期に合わせた 2022 年 3 月の 10 日間で 3 つのコミットにより適用された
- Poetry の防御は 2021 年 9 月に追加され、1 年後に CVE が割り当てられ、さらに 6 か月後に dulwich へ移行した
- vcpkg は例外的に、Git レジストリ対応が書かれた 初日から
--を使用していた
最小 Git バージョンが生む互換性制約
- Composer の CVE-2022-24828 アドバイザリは
--end-of-optionsを正しい修正と位置付けているが、より古い Git もサポートする必要があるため、ダッシュで始まるブランチ名を拒否する方式を選んだ - vcpkg の Git 統合は最小バージョンを Git 2.7.4 と明記しており、Homebrew の Linux 向け
HOMEBREW_MINIMUM_GIT_VERSIONは 2018 年に設定された 2.7.0である - Git 2.14.3 を提供していた Amazon Linux 2 が 2026 年 6 月にサポート終了を迎え、こうした下限が追跡していたディストリビューションがようやくサポート範囲から外れつつある
- Ubuntu の長期サポート状況も一斉移行を難しくしている
- Ubuntu 18.04 は Git 2.17.0 を提供し、2028 年まで延長サポートされる
- Ubuntu 20.04 は Git 2.25.1 を提供し、2030 年まで延長サポートされる
- Git 2.25.1 は
git fetchの--end-of-optionsは受け付けるが、git rev-parseでは拒否する
--end-of-optionsに依存するには、多くのサブコマンドで Git 2.24.0、rev-parseは 2.30.0、checkoutとresetは 2.43.1 を最小バージョンとして要求する必要がある- 最小バージョンを引き上げると、ディストリビューション同梱の古い Git を使うユーザーをサポートできなくなる
プロセス実行の代わりに Git ライブラリを使う
- libgit2、gitoxide、go-git、JGit、dulwich は、プロセス内部で clone と fetch に必要な Git 転送プロトコルを実装している
- 別個の
argv境界がないため、引数リストに注入される対象そのものが存在しない - Jujutsu は Git 連携に gitoxide を使っており、引数インジェクション系の公開 CVE はない
- 現時点までのアドバイザリ 2 件は、パストラバーサルと、ライブラリ由来の SHA-1 衝突検査欠落に関するものだ
- go-git の CVE-2025-21613 は
file://転送に限定される- この経路は go-git が Git バイナリを実行する唯一のコードパスである
- 独自の Git 実装を含めると、upstream Git が出すチェックアウト安全性の修正をすべて追跡する必要があり、libgit2 と JGit のどちらでもこれに関連する修正が繰り返し発生している
- このコストは現実に存在するが、各呼び出し箇所で永続的に引数検査を覚えておく代わりに、具体的な upstream パッチの流れを適用する問題へと変わる
Homebrew に提案された変更範囲
- Homebrew PR は最小 Git バージョンを 2.30.0 に引き上げ、次の箇所に
--end-of-optionsを追加するclone、remote set-url、ls-remoteの URL 前rev-parseの ref 前
checkoutとresetの呼び出しは変更しない- この 2 コマンドまで保護するには、2024 年 2 月にリリースされた Git 2.43.1 が必要
- このバージョンは、現在サポートされている複数のディストリビューションが提供する Git より新しい
1件のコメント
Lobste.rs のコメント
特に、ある
jjコマンドで学んだ知識が他のコマンドにも自然に適用される。git logのドキュメントは、出力オプションごとのフラグが爆発的に増えているうえ、コミット範囲構文である「特殊表記」と追加のフィルターフラグまで混在しており、他の Git コマンドに転用できる知識も少ない一方、
jj logのドキュメントは、1ページでも余白が残るほど簡潔。Git の寄せ集め的な複雑さを リビジョンセット・ファイルセット・出力テンプレート DSL の3つに置き換えており、これらが jj 全体で一貫して使われるため、はるかに単純で組み合わせやすく直感的。Git が20年間にわたって余計なものを積み重ねてきた点は考慮すべきだが、jj はそれを避けるうえではるかに有利に見える--が必要だということは分かるが、ここに 独自の変換ルール まで要求すると、あまりに危険な落とし穴になる--を選んだ理由が気になる。ファイルを明示的な引数として受け取る単純な方式ではなく、これを選んだことに、表には見えない 設計上のトレードオフ があったのか疑問1990年代の Tcl と Scheme の論争を振り返ると、この場合は「悪い方が良い」が正しかったのかもしれない。JSON や XML などを含む S 式系が UNIX エコシステムにはほとんど定着しなかった一方で、Tcl には文字列の中にサブ言語を階層化する比較的原則的なアプローチがあった