1 ポイント 投稿者 GN⁺ 6 시간 전 | 1件のコメント | WhatsAppで共有
  • Git の -- は一般的なオプション終端子ではなく、リビジョンとパス指定を区別するためのものなので、信頼できないリビジョンを安全に渡すには Git 2.24.0 からサポートされた --end-of-options が必要
  • git log --end-of-options "$rev" -- "$path" では、前の記号は オプションとリビジョン、後ろの -- はリビジョンとパスを区別しており、相互に置き換えられない
  • シェルを介さずに argv 配列を直接実行しても、ダッシュで始まる入力が --upload-packcore.sshCommandProxyCommand のようなオプションとして解釈されると、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 foofoo というブランチとファイルのどちらを意味するのか曖昧
    • git log main -- README.md は、main のコミットのうち README.md を変更したコミットを意味する
  • この設計のため、リビジョン位置にはオプション終端の記号がなく、git log "$rev"$rev がダッシュで始まると Git はそれをオプションとして解釈する
  • --end-of-options を導入したコミットは、既存の -- がすでにリビジョンとパス指定を分けているため、オプションとリビジョンを区別する別の記号が必要だと説明している
  • --end-of-optionsgitcli(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 checkoutgit 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/execargv 配列を使い、シェルを経由しない事例だった
    • Git コンテキスト URL の #ref:dir フラグメントが git fetch origin <ref> に渡され、その <ref>--upload-pack=<cmd> として解釈された

複数のバージョン管理システムで繰り返された脆弱性

  • 2017 年 8 月の同じ日に、4 つのバージョン管理システムで同一パターンが公開された
  • 4 つのシステムはいずれも URL のホスト名を SSH 引数として渡しており、-oProxyCommand= で始まるホスト名が SSH オプションとして処理された
  • Phabricator の事後分析によると、当時活発に保守されていた 3 つのツールのうち、Subversion だけがホスト名の前に -- を追加していた
    • Git と Mercurial は、すべての SSH 実装が -- をサポートするわけではないという理由もあり、ホスト名形式を検証していた
  • -- が欠けたコードでも、引数がダッシュで始まるまでは正常に見えて動作するため、この仕組みは 本質的に安全ではない

パッケージマネージャが露出する経路

  • パッケージマネージャはマニフェスト、ロックファイル、推移的依存関係メタデータから Git URL や ref を受け取り、サブプロセスに渡す
    • Gemfile の gem 'foo', git: '...'
    • package.jsongithub:user/repo#ref
    • pyproject.tomlCargo.tomlmix.exsPackage.swiftpubspec.yamlconanfile.pygo.mod の同等設定
  • 2026 年 7 月時点の HEAD を基準に調査した 19 個のパッケージマネージャのうち 17 個が、デフォルトまたは唯一の経路として Git バイナリを実行していた
  • 残る 2 つはライブラリをデフォルトで使っている
    • Cargo は libgit2 を使用し、net.git-fetch-with-cli を有効にすると Git プロセスを実行する
    • Poetry は 1.2.0 から dulwich に移行しており、system-git-client 設定でシステム Git を使える
  • 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

実際の防御状況と 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 clone URL 前の -- は、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_VERSION2018 年に設定された 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.0rev-parse は 2.30.0、checkoutreset は 2.43.1 を最小バージョンとして要求する必要がある
  • 最小バージョンを引き上げると、ディストリビューション同梱の古い Git を使うユーザーをサポートできなくなる

プロセス実行の代わりに Git ライブラリを使う

  • libgit2、gitoxidego-gitJGit、dulwich は、プロセス内部で clone と fetch に必要な Git 転送プロトコルを実装している
  • 別個の argv 境界がないため、引数リストに注入される対象そのものが存在しない
  • Jujutsu は Git 連携に gitoxide を使っており、引数インジェクション系の公開 CVE はない
    • 現時点までのアドバイザリ 2 件は、パストラバーサルと、ライブラリ由来の SHA-1 衝突検査欠落に関するものだ
  • go-git の CVE-2025-21613file:// 転送に限定される
    • この経路は go-git が Git バイナリを実行する唯一のコードパスである
  • 独自の Git 実装を含めると、upstream Git が出すチェックアウト安全性の修正をすべて追跡する必要があり、libgit2 と JGit のどちらでもこれに関連する修正が繰り返し発生している
  • このコストは現実に存在するが、各呼び出し箇所で永続的に引数検査を覚えておく代わりに、具体的な upstream パッチの流れを適用する問題へと変わる

Homebrew に提案された変更範囲

  • Homebrew PR は最小 Git バージョンを 2.30.0 に引き上げ、次の箇所に --end-of-options を追加する
    • cloneremote set-urlls-remote の URL 前
    • rev-parse の ref 前
  • checkoutreset の呼び出しは変更しない
    • この 2 コマンドまで保護するには、2024 年 2 月にリリースされた Git 2.43.1 が必要
    • このバージョンは、現在サポートされている複数のディストリビューションが提供する Git より新しい

1件のコメント

 
GN⁺ 6 시간 전
Lobste.rs のコメント
  • 最近ますます jj にハマっている。こうした Git の雑然さと比べると安心できるほどで、まだ習得中ではあるものの、意図がよく反映され、望む作業方法も簡単に把握できる
    特に、ある jj コマンドで学んだ知識が他のコマンドにも自然に適用される。git log のドキュメントは、出力オプションごとのフラグが爆発的に増えているうえ、コミット範囲構文である「特殊表記」と追加のフィルターフラグまで混在しており、他の Git コマンドに転用できる知識も少ない
    一方、jj log のドキュメントは、1ページでも余白が残るほど簡潔。Git の寄せ集め的な複雑さを リビジョンセット・ファイルセット・出力テンプレート DSL の3つに置き換えており、これらが jj 全体で一貫して使われるため、はるかに単純で組み合わせやすく直感的。Git が20年間にわたって余計なものを積み重ねてきた点は考慮すべきだが、jj はそれを避けるうえではるかに有利に見える
  • コマンドではユーザー提供の引数の前に通常 -- が必要だということは分かるが、ここに 独自の変換ルール まで要求すると、あまりに危険な落とし穴になる
  • ツールを過度に複雑にしたことの正直な結果だ。Git は新たに登場した バザール のように見える
  • そもそもファイル引数の曖昧さを解消するために -- を選んだ理由が気になる。ファイルを明示的な引数として受け取る単純な方式ではなく、これを選んだことに、表には見えない 設計上のトレードオフ があったのか疑問
    • すでに確立された UNIX のオプション解析の慣例 をまったく別の用途に使ったのは、予想どおり愚かで、いかにも Git らしい選択
  • UNIX の すべてはテキスト という哲学がまた問題を引き起こしている。コマンドラインは構造化データの完璧な例であるにもかかわらず、この落とし穴から抜け出すための共同調整コストが大きすぎるようだ
    1990年代の Tcl と Scheme の論争を振り返ると、この場合は「悪い方が良い」が正しかったのかもしれない。JSON や XML などを含む S 式系が UNIX エコシステムにはほとんど定着しなかった一方で、Tcl には文字列の中にサブ言語を階層化する比較的原則的なアプローチがあった