GitPythonの`Remote.pull()`メソッドにおいて、`refspec`引数が安全性のためのチェックを迂回し、任意のコマンド実行を可能にする脆弱性が見つかりました。
## 概要
GitPythonの`Remote.pull()`メソッドは、位置引数として渡された`refspec`を、`git pull`のサブプロセスにそのまま引き渡します。しかし、このメソッドの安全性のチェックは、キーワード引数のみを対象としており、位置引数である`refspec`はチェックされません。そのため、攻撃者が制御できるデータ(例えば、ウェブサービスやCIサービスで提供されるブランチ名や`refspec`パラメータ)を`refspec`として渡すことで、`--upload-pack=<コマンド>`のようなオプションを含む値を挿入できます。この値は、`unsafe_git_pull_options`のチェックを迂回し、`git pull`コマンドの行の`--`区切り文字の後に配置され、ローカルパスまたはSSH接続におけるアップロードプログラムとして実行されます。これにより、呼び出し元のプロセスの権限で任意のコマンドを実行できてしまいます(デフォルトでは`allow_unsafe_options=False`が設定されています)。
## 根本原因
`Remote.pull()`メソッドは、`allow_unsafe_options=True`が設定されていない場合、危険なオプションを拒否するように記述されています。`--upload-pack`は、そのようなオプションとして明示的にリストされています。しかし、このチェックはキーワード引数のみを対象としており、位置引数である`refspec`はチェックされません。チェックの処理では、`Git._option_candidates([], kwargs)`が呼び出され、位置引数の代わりに空のリストが渡されるため、`refspec`の値は`unsafe_git_pull_options = ["--upload-pack"]`と比較されません。その結果、正規化された`refspec`がそのまま`git pull`コマンドに引き渡されます。`git fetch`や`git push`においては、オプションとして解釈される`refspec`を無効にするための`--`区切り文字が存在しますが、`git pull`には適用されません。なぜなら、`git pull`は内部的に`git fetch`を呼び出す際に、その後の位置引数を`--`区切り文字なしで再利用するため、位置引数は内部の`git fetch`のオプションとして扱われてしまうからです。
```text
影響を受ける関数: git.remote.Remote.pull
ソース: git/remote.py (チェックの箇所は`_option_candidates([], kwargs)`の呼び出し、実行の箇所は`self.repo.git.pull("--", self, refspec, ...)`の呼び出し)
```
## 影響
`Remote.pull()` に渡される `refspec` の値を攻撃者が制御できる場合、GitPython を呼び出すプロセスとして、追加の権限やユーザーの操作なしに、デフォルトのコードパスで任意のオペレーティングシステムコマンドを実行できてしまう可能性があります。この脆弱性を利用するには、プルされるリモートリポジトリが、ローカル側で `upload-pack` を実行するトランスポートを使用している必要があります。具体的には、ローカルファイルシステムパスまたは SSH です。HTTPS の場合、クライアントから提供される `upload-pack` プログラムはサーバーによって無視されるため、`--upload-pack` の値は無効になります。このトランスポートの要件により、脆弱な環境の範囲は限定されますが、攻撃者が脆弱なターゲットに到達した場合、追加の作業は必要ありません。多くのローカルリポジトリでは、ローカルパスまたは SSH を使用しているため、影響を受ける可能性のある環境は比較的多いです。
## 実証
```text
環境: 影響を受けているソースツリーからの GitPython; システムの git 2.43.0。
プルされるオリジンが、ローカルファイルシステムトランスポートを使用しています (git >= 2.38 の場合、`protocol.file.allow=always`)。
```
再現手順:
1. ローカルパストランスポートでアクセス可能なオリジンリポジトリを作成します。
2. クライアントリポジトリで、そのオリジンを登録し、攻撃者が制御する `refspec` を指定して `Remote.pull()` を呼び出します。
3. 注入されたコマンドが実行されることを確認します。
```python
import git
repo = git.Repo("consumer") # ローカルパスまたは SSH トランスポートを使用するオリジンを持つ
origin = repo.remote("origin")
# 攻撃者が制御する refspec の値; デフォルトの引数 (allow_unsafe_options=False):
origin.pull(refspec="--upload-pack=touch /tmp/pwned; git-upload-pack")
```
```text
cmdline: git pull -v -- origin --upload-pack=touch /tmp/pwned; git-upload-pack
[attack] marker created (RCE) : True
[isolate] upload_pack passed as KWARG blocked : True (only the positional refspec escapes)
[sibling] same value via git fetch : safe (pull-specific behavior)
[control] benign refspec : safe
```
## 攻撃の仕組み
1. **脆弱性:** アプリケーションが、デフォルトのパスで `Remote.pull(refspec=<value>)` を呼び出す際、`<value>` が攻撃者によって操作された入力(例えば、ブランチやリファレンスペックのリクエストパラメータ)から派生している。
2. **制御:** 攻撃者が文字列 `--upload-pack=<command>; git-upload-pack` を提供する。この値は `Git._unpack_args` を通過するが、ここでは文字列化されるだけで、オプションの構造は維持される。
3. **パス:** プロトコルのループが、この値に対して `Git.check_unsafe_protocols` を呼び出す。これは、リモートヘルパーのトランスポートURL(`<transport>::` の形式)にのみ一致し、`--upload-pack=` というトークンを検出しない。
4. **防御:** 不安全なオプションのチェック機構が、`check_unsafe_options(options=Git._option_candidates([], kwargs), ...)` を呼び出す。引数として渡されるリストが空であるため、位置引数としての `refspec` は `unsafe_git_pull_options` に対してチェックされない。
5. **根本原因:** この値が `git pull -v -- <remote> --upload-pack=<command>; git-upload-pack` に渡される。`git pull` は、位置引数を内部の `git fetch` に再渡すが、この際 `--` で区切られていないため、`upload-pack` プログラムとして扱われる。
6. **結果:** ローカルパスまたはSSHトランスポートの場合、`git` は `<command>` を `upload-pack` プログラムとして実行し、呼び出し元のプロセスのユーザー権限で任意のコマンドを実行できる。
## 脆弱性の検証
関連する防御機構は、`Remote.pull()` 自体の `check_unsafe_options` の呼び出しであり、これは現在のリリース版に含まれている。しかし、この機構は不完全である。`Git._option_candidates([], kwargs)` が呼び出されるが、これはキーワード引数のみを考慮するため、位置引数としての `refspec` はチェックされない。これは実験的に確認されている。同じ値を `upload-pack=` というキーワード引数として渡すと `UnsafeOptionError` が発生するが、位置引数としての `refspec` として渡すとエラーが発生しない。これにより、位置引数のみが防御機構を回避できることが証明される。
`git pull`コマンドの引数リストにおける`--`区切り文字の効果について検証が行われましたが、効果は認められませんでした。システム側のGitバージョンが2.43.0の場合、`git pull -- <remote> '--upload-pack=<cmd>'`を実行すると、注入されたコマンドが実行されました。しかし、同じ値を`git fetch -- ...`や`git push -- ...`に与えた場合、コマンドは実行されませんでした。これは、これらのサブコマンドが`--`区切り文字を尊重するのに対し、`git pull`は引数を内部のフェッチ処理にそのまま渡すためです。また、`check_unsafe_protocols`というトランスポートチェックも検証されましたが、`--upload-pack=`というトークンは、`<transport>::`という形式ではないため、フラグが立てられませんでした。このパッケージに関する過去の公開されたセキュリティ情報には、`Remote.pull`、フェッチまたはプルリファレンスの引数、またはリファレンス経由での`--upload-pack`の注入に関する記述はありません。最も近い関連のある問題は、別の関数と処理において、同様の脆弱性(キーワード専用のガードを回避する引数)が存在することを示しています。
## 対象バージョン
- `Ecosystem: pip (PyPI)`
- `Package: GitPython`
- `確認された影響範囲: >= 3.1.30`
- `最終確認されたバージョン: 3.1.62 (PyPI); ソースコードは3.1.62のリリースタグと比較して検証済み`
- `修正状況: リリースされているバージョン、または開発ブランチのいずれにも修正は含まれていません`
キーワード専用の引数チェックと、位置引数として扱われる`refspec`の組み合わせは、3.1.30から最新版の3.1.62までのすべてのリリースバージョンに存在します。これは、各リリース版に含まれる`git/remote.py`ファイルを検証することで確認されました。バージョン3.1.30で`unsafe_git_pull_options`という引数チェックが導入されました。影響範囲は、このチェックの導入という観点から、3.1.30以降とされています。3.1.30以前のバージョンでは、`refspec`が位置引数として渡され、安全なオプションのチェックも`--`区切り文字も使用されていないため、同様に脆弱性が存在しますが、影響範囲は保守的に、チェックの導入境界から定義されています。修正されたバージョンは存在しません。
## 推奨される対策
位置引数だけでなく、キーワード引数に対しても、`unsafe-option`の制約を適用する。この制約は、実際にフォワードされた位置引数に基づいて候補リストを構築する(例えば、正規化されたrefspecリストを`Git._option_candidates`に`kwargs`と共に渡すなど)ことで実現される。これにより、`--upload-pack=...`のようなオプション形式の`refspec`は、`allow_unsafe_options=True`が設定されていない限り、`UnsafeOptionError`で拒否される。同様の修正は、キーワード引数のみを受け付けるように設計された他の参照またはrefspecのフォワード処理においても検証する必要がある。コードの変更なしの一時的な対策として、呼び出し元は、`-`で始まるrefspecの値を検証または拒否し、信頼できないデータを`refspec`として渡さないようにし、クライアントが提供するアップロードプログラムが無効なHTTPSリモートを使用することを推奨する。
報告者:zx (GitHub: @manus-pi)。
影響を受けるパッケージ:
- pip (Python) GitPython: 影響あり。バージョン3.1.30以上、3.2.0未満。3.2.0で修正済み。