プログラミング
Gitの「オプション終了」フラグ --end-of-options
git's –end-of-options Flag (nesbitt.io)
要約
Gitのあまり知られていない「--end-of-options」フラグは、コマンドライン引数におけるセキュリティ上の脆弱性、特に引数インジェクションを防ぐために導入されました。このフラグは、オプション解析とリビジョンやパス指定を明確に区別するために使用され、パッケージマネージャーなどが外部からの信頼できない入力を安全に処理する上で重要です。
全文翻訳
先週、あるパッケージマネージャーのCVE(共通脆弱性識別子)の修正を読んでいたところ、これまで気づかなかったGitのフラグ「--end-of-options」に出くわしました。最初はLLM(大規模言語モデル)が幻覚を見たのかと思いましたが、gitcli(7)に記載があり、2019年11月のGit 2.24.0で追加されたもので、Gitが既に「--」を別の目的で使用していたために存在します。
ほとんどのUnixツールでは、「--」はオプション解析の終了を示し、例えば rm -- -f は、forceフラグを渡すのではなく、-f という名前のファイルを削除します。Gitは初期の頃に「--」をリビジョンとパス指定を区別するために再利用しました。なぜなら、git log foo だけでは、foo という名前のブランチと foo という名前のファイルとの間で曖昧さが生じるため、どちらか一方の解釈にマーカーが必要だったからです。git log main -- README.md は、そのファイルに影響を与えたmainブランチのコミットを意味します。
これにより、リビジョン位置に終端記号がなくなり、スクリプトが git log "$rev" を実行し、$rev がダッシュで始まる場合、Gitはそれをオプションとして解析してしまいます。--end-of-options を導入したコミットによると、「しかし、それはリビジョンパーサーには機能しません。なぜなら、-- は既にそこで意味を持っているからです。それはリビジョンとパス指定を区別します。したがって、オプションとリビジョンを区別するためには、別のマーカーが必要です。」
Gitにおいて「--」と「--end-of-options」は異なるものであり、それらを互換性があるものとして扱うのは、私が既にいくつかの場所で見かけた間違いです。git clone -- "$url" の前に「--」を置くと、cloneはPOSIX規約に従うため機能します。refの後の末尾の「--」、例えば git checkout "$ref" -- は、$ref をファイル名ではなくリビジョンとしてマークしますが、それでも最初にオプションとして読み取られる可能性があります。信頼できないリビジョンを安全に渡すには、git log --end-of-options "$rev" -- "$path" と記述する必要があります。両方のマーカーが別々の役割を果たします。
新しいフラグのサポートは、一度にすべてではなく、サブコマンドごとに到着しました。git rev-parse は、独自のカスタム引数パーサーを持っているため、最初のリリースから1年後の2.30.0でしか対応しませんでした。また、git checkout と git reset は、2024年2月の2.43.1までこれを拒否しました。なぜなら、それらは「--」自体を解析し、初期実装では「--end-of-options」が引数リストに残ってしまい、それらのパーサーが拒否したからです。
引数インジェクション
Git、Mercurial (hg)、SSHはいずれも、呼び出し元が指定したコマンドを実行するという文書化された目的を持つオプションを提供しています。git clone は、サーバー側のバイナリを指定するために --upload-pack=<cmd> を受け付け、任意のGit呼び出しは、接続方法を上書きするために -c core.sshCommand=<cmd> を受け付けます。Mercurialは、任意のサブコマンドで --config=alias.<subcmd>=!<shell> を受け付け、実行中のサブコマンドを任意のシェルスクリプトとして再定義します。SSHは -oProxyCommand=<cmd> を受け付けます。
これらは文書化された機能ですが、ラッパープログラムが信頼できない文字列を引数リストに渡すと、攻撃プリミティブ(攻撃の基盤)になります。この失敗モードには、CWE-88(引数インジェクション)という独自のCWEがあります。これはコマンドインジェクションとは異なります。なぜなら、シェルは関与しないからです。ラッパープログラムはargv配列を構築し、execを直接呼び出します。これは「system()を使用しないでください」というすべてのガイドが推奨する方法であり、配列はGitにそのまま到達し、Gitはその引数の一つを開始ダッシュで解析してオプションとして扱います。
CVE-2019-13139におけるDocker buildの例は、クリーンなケースです。Goのos/execパッケージはargv配列を使用し、シェルは使用しません。そして、GitコンテキストのURLの#ref:dirフラグメントが、git fetch origin <ref> の --upload-pack=<cmd> としてGitに渡されました。
このパターンは、2017年8月のある日に4つのバージョン管理システムで実証されました。CVE-2017-1000117 (Git)、CVE-2017-1000116 (Mercurial)、CVE-2017-9800 (Subversion)、CVE-2017-12836 (CVS) が同時に開示されました。それぞれがURLのホスト名をSSHの引数として渡し、-oProxyCommand= で始まるホスト名がSSHオプションになりました。Phabricatorの開示後の分析によると、アクティブにメンテナンスされている3つのツールの中で、Subversionだけが修正でホスト名の前に「--」を追加しました。GitとMercurialはホスト名のフォーマットを検証しましたが、これは「--」がすべてのSSH実装でサポートされているわけではないためでもあります。同じレポートは、「--」メカニズム自体を「デフォルトで安全ではない」と呼びました。なぜなら、それがないコードは正しく見え、引数がダッシュで始まるまで問題なく動作するからです。
パッケージマネージャー
パッケージマネージャーは、通常、Git URLまたはリファレンスをデータとして受け取り、サブプロセスに渡します。例えば、Gemfileのgem 'foo', git: '...'、package.jsonのgithub:user/repo#ref、pyproject.toml、Cargo.toml、mix.exs、Package.swift、pubspec.yaml、conanfile.py、go.modの同等のものなどです。URLとリファレンスは、マニフェスト、ロックファイル、または推移的な依存関係のメタデータに到着します。
私がチェックした19のパッケージマネージャーのうち、17はデフォルトまたは唯一のパスとしてGitバイナリをフォーク(子プロセスとして起動)します。ライブラリをデフォルトとする2つは、Cargo(libgit2を使用し、net.git-fetch-with-cli設定でフォークをオプトイン可能)と、Poetry(1.2.0でdulwichに切り替え、system-git-client設定でフォールバック可能)です。
Nixはローカルリポジトリの読み取りにはlibgit2を使用しますが、フェッチにはGitをフォークします。なぜなら、libgit2はgit-credentialヘルパーのサポートを欠いているからです。このクラスのパッケージマネージャーに対する公開されたCVEには、CVE-2021-43809 (Bundler)、CVE-2021-29472 および CVE-2022-24828 (Composer)、CVE-2022-36069 (Poetry)、CVE-2023-5752 (pip)、CVE-2022-21223 および CVE-2022-24440 (CocoaPods)、CVE-2025-68119 (Go) が含まれます。
いくつかの2022年のエントリを生み出したSnykの研究はここ([https://snyk.io/blog/git-security-vulnerabilities-and-how-to-fix-them/](https://snyk.io/blog/git-security-vulnerabilities-and-how-to-fix-them/))にまとめられており、Sonarはバイナリごとの危険なオプションのカタログを維持しています。
Gitをフォークする17のうち、正確に1つだけが「--end-of-options」を使用しています。それはGoのcmd/goです。Goは2019年6月にリポジトリURLの前に「--」を追加し、一般的な強化パスとしました。2026年1月にそれが不十分であることが判明し、「--end-of-options」が全体にわたって追加され、CVE-2025-68119の修正となりました。これには、2017年のhg 4.4.2以降、Mercurialの早期オプション解析を制限してきたHGPLAIN=+strictflagsも含まれます。
コミットメッセージは次のように締めくくられています。「将来的にはこれらの問題を誤って再導入することをより困難にするための、より構造化された変更をフォローアップすべきですが、今のところこれは手元の問題を解決します。」
最小Gitバージョン
その他のパッケージマネージャーは、引数リストを「--」または入力の先頭ダッシュチェックで保護していますが、各ガードが追加された時期を見ると、ほとんどは元の実装ではなく、報告された脆弱性の修正として到着しました。
Bundlerのgit cloneにおけるURL前の「--」は、CVE-2021-43809のパッチです。Cocoapods-downloaderにおける先頭ダッシュの拒否は、2022年3月の10日間にわたる3つのコミットで実施され、CVE-2022-21223の開示と一致しています。Poetryのガードは2021年9月に到着し、1年後にCVEが割り当てられ、その6ヶ月後にdulwichへの切り替えが行われました。
vcpkgは、gitレジストリサポートが書かれたその日から「--」が存在していた例外です。
ComposerのCVE-2022-24828に関するアドバイザリは、なぜこれらのツールのほとんどが「--end-of-options」を使用しないのかを説明しています。それはフラグを正しい修正として名前を挙げ、次にComposerがそれを先行するGitバージョンをサポートしているため、パッチは先頭ダッシュのブランチ名を拒否すると述べています。
vcpkgのGit統合には、Git 2.7.4のフロアを指定するコメントがあります。HomebrewのHOMEBREW_MINIMUM_GIT_VERSIONはLinuxで2.7.0であり、2018年に設定されました。Git 2.14.3をパッケージ化していたAmazon Linux 2は、先月サポートが終了したため、それらのフロアが追跡するディストリビューションは今や古くなっています。
Git 2.17.0を搭載したUbuntu 18.04は、2028年まで延長サポート中です。2030年まで延長サポート中のUbuntu 20.04は、2.25.1をパッケージ化しており、これはgit fetchで「--end-of-options」を受け入れるには十分新しいですが、git rev-parseで拒否するには古すぎます。
このフラグに依存することは、ほとんどのサブコマンドでGit 2.24.0、rev-parseで2.30.0、checkoutとresetで2.43.1に最小バージョンを引き上げ、まだディストリビューションパッケージのGitを使用しているユーザーを失うことを意味します。
Gitライブラリ
libgit2、gitoxide、go-git、JGit、dulwichはいずれも、Gitワイヤプロトコルの十分な実装を持っており、インプロセスでクローンおよびフェッチを実行できます。これにより、argvの境界がなく、インジェクションされる引数リストも存在しません。
JujutsuはGitの相互運用にgitoxideを使用しており、引数インジェクションクラスでの公開されたCVEはありません。これまでの2つのアドバイザリは、パス・トラバーサルと、ライブラリから継承したSHA-1衝突チェックの欠如です。
go-gitにはCVE-2025-21613というものがあり、それは仕様(specifi)