Dulwich: チェックアウト時のシンボリックリンクに対する書き込み処理において、rawな`os.open`の使用により、すべてのシンボリックリンク保護機能を回避する。
## 概要
Dulwichの`porcelain.checkout(paths=[...])`のコードパスでは、ファイルを書き込む際に、rawな`os.open(file_path, O_WRONLY|O_CREAT|O_TRUNC, mode)`を使用し、その後`f.write(obj.data)`を実行する。このコードパスでは、`build_file_from_blob()`が一切呼び出されず、シンボリックリンクに対する保護機能が完全に回避される(未修正のd09f8afの修正も含まれる)。`O_NOFOLLOW`を指定しない`os.open`は、ターゲットファイルと中間ディレクトリの両方でシンボリックリンクを辿り、これにより任意のファイルへの書き込みが可能になる。
## 根本原因
`dulwich/porcelain/__init__.py`の5661行から5675行にある`checkout(paths=[...])`の実装は以下の通り:
```python
file_path = _checked_worktree_path(r, path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(file_path, flags, mode), "wb") as f:
f.write(obj.data)
```
`_checked_worktree_path()`(601行から631行)は、ファイル名の検証のみを行い、パスが`/`または`\\`で始まらないこと、およびコンポーネントが`INVALID_DOTNAMES`のチェックを通過することを確認する。ファイルシステム上のシンボリックリンクの検出は行われない。
## 影響
攻撃者は、悪意のあるリポジトリを作成し、被害者がそれをクローンして`checkout(paths=[...])`を実行すると、攻撃者が制御する内容(攻撃者が制御する権限付き)を、ユーザーがアクセス可能なファイルシステム上の任意の場所に書き込むことができる。`.git/hooks/post-checkout`に書き込むことで、次のgitチェックアウト時にリモートコード実行(RCE)が可能になる。
## 攻撃シナリオ
1. 攻撃者は、HEADが`trigger`というシンボリックリンク(モード120000、内容`../../.git/hooks/post-checkout`)を指し、タグ`v1.0`が`trigger`という実行可能ファイル(モード100755、内容`#!/bin/sh\nmalicious_payload`)を指すリポジトリを作成します。
2. 攻撃対象者はリポジトリをクローンします。ワークツリー内の`trigger`が`../../.git/hooks/post-checkout`(シンボリックリンク)を指します。
3. 攻撃対象者は、特定のファイル(タグ`v1.0`)を復元するために、`porcelain.checkout(repo, target="v1.0", paths=["trigger"])`を実行します。
4. `_checked_worktree_path(r, "trigger")`が正常に完了します。これは名前の検証のみで、シンボリックリンクのチェックは行われません。
5. `os.open("trigger", O_WRONLY|O_CREAT|O_TRUNC, 0o755)`がシンボリックリンクに従い、`.git/hooks/post-checkout`を書き込み用に開きます。
6. `f.write(obj.data)`が、悪意のあるペイロードをフックに書き込みます。
7. 次のチェックアウト操作がフックをトリガーし、リモートコード実行(RCE)が発生します。
## 修正案
`os.open`で直接指定しているパスを、`build_file_from_blob`関数への呼び出しに置き換えます(ただし、この関数は中間的なシンボリックリンクに対する保護が施されている必要があります)。または、明示的なシンボリックリンク検出を追加します。パスを`os.path.realpath()`で解決し、ファイルを開く前に、それがワークツリーのルート内に留まっていることを確認します。
報告者:**zx (Jace)**
影響を受けるパッケージ:
- pip (Python) dulwich: 影響範囲は0.24.0以上、1.2.7以下。1.2.8で修正済み。