GitPython: リポジトリの内容がgitディレクトリを偽装し、任意のコードの実行につながる可能性がある
### 概要
`Repo.__init__` は、候補となるパスを順番にテストすることで、どのディレクトリがgitディレクトリであるかを判断します。ただし、実際の `.git` ディレクトリは最後に評価されます。最初の2つのテストは、通常の追跡対象ファイルによっても満たされる可能性があります。Gitは、リテラルな名前 `.git` のみを使用するため、リポジトリのルートにある `HEAD`、`objects/`、`refs/`、`config`、`gitdir`、`commondir`、および `hooks/` はすべて、有効な追跡対象コンテンツです。
その結果、被害者が攻撃者のリポジトリを開いたりクローンしたりすると、GitPython は `git_dir` を **ワーキングツリーのルート** に解決しますが、実際のgitは `<root>/.git` を正しく解決します。GitPython がその後「gitディレクトリ内にある」と見なすものはすべて、攻撃者が作成したコンテンツであり、`hooks/` も含まれます。GitPython は、`hooks/` 内のファイルを実行します。
### CVE-2026-87817
### 影響を受けるコード (3.1.59)
`git/repo/base.py` 内の探索ループは、以下の順序でテストを行います。
1. `git/repo/base.py:299` — `isfile(curpath/gitdir)` **かつ** `isfile(curpath/commondir)` **かつ** `isfile(curpath/HEAD)`
2. `git/repo/base.py:320` — `is_git_dir(curpath)`
3. `git/repo/base.py:341` — `dotgit = osp.join(curpath, ".git")` ← 実際のgitディレクトリ。これは最後に評価されます。
`is_git_dir` (`git/repo/fun.py:60`) は、`objects/` と `refs/` がディレクトリであり、`HEAD` がファイルであるという条件のみを満たせば良いです。**`HEAD` の内容は解析されません。** フックのパスは、`index.repo.git_dir` (`git/index/fun.py:73`)、つまり誤って解決されたディレクトリから解決されます。
### 実証コード
`pip install GitPython==3.1.59` のみが必要です。完全なスクリプトは `poc1_rce.py` として添付されています。このスクリプトは、一時ディレクトリ内で完全に実行され、ペイロードはマーカーファイルのみを書き込みます。
攻撃者のリポジトリ — ルートに4つの通常の追跡対象ファイルがあります。
| パス | モード | コンテンツ |
|---|---|---|
| `gitdir` | 100644 | `.git\n` |
| `commondir` | 100644 | `.git\n` |
| `HEAD` | 100644 | `ref: refs/heads/master\n` |
| `hooks/pre-commit` | **100755** | `#!/bin/sh` + ペイロード |
被害者 — 2つの通常の呼び出し:
```python
repo = git.Repo.clone_from(url, dst) # または git.Repo(dst)
repo.index.commit("自動コミット") # ここでコードの実行が行われる
```
PyPIのバージョン3.1.59(LinuxとWindows)で確認された現象:
```
gitが示すgitディレクトリ:/tmp/.../victim/.git
GitPythonが示すディレクトリ:/tmp/.../victim ← 隠蔽されている
攻撃者のフックが実行されたか:True
git fsck:(クリーン)
```
`pre-commit`フックは`git/index/base.py:1201`で実行され、`write_tree()`の前に実行されるため、
後でコミットが失敗した場合でも実行されます。
### 影響
GitPythonを使用して、信頼できないリポジトリを開いたりクローンしたりするサービス(CIランナー、フォークプルリクエストのビルド、コードスキャン/SBOMサービス、ミラー、依存関係ボット、AIコードレビュー/エージェントツールなど)は、以下のような影響を受ける可能性があります。
1. **任意のコマンドの実行**: ターゲットの`<root>/hooks/pre-commit`が実行されます。これは、被害者が`index.commit()`を呼び出したときに発生します。
2. **リポジトリ外のファイルの読み取り**: 追跡されている`<root>/config`がリポジトリの設定ファイルとなり、`merge_includes=True`(`git/repo/base.py:765`)で解析されます。そのため、`[include] path = ~/.aws/credentials`のような設定があると、そのファイルの内容が漏洩する可能性があります。(`Repo._config_reader`では、依然としてデフォルトで`merge_includes=True`が設定されているため、3.1.59で追加された`.gitmodules`/GHSA-7833に対する対策は、このパスをカバーしていません。)
3. **攻撃者が指定したディレクトリへの設定ファイルの書き込み**: 追跡されている絶対パスの`commondir`を使用することで、設定ファイルを攻撃者が指定したディレクトリに書き込むことができます。
攻撃は目立たない形で実行されます。`git clone`は正常終了(終了コード0)、`git fsck`(`--strict`オプション付き、および`transfer.fsckObjects`/`fetch.fsckObjects=true`を設定した場合も)は何も報告せず、クローンはすべての読み取り専用のチェック(`head.commit`、`branches`、`is_dirty()`、`untracked_files`、`iter_commits`)をパスします。これは、追跡されている`.git`ディレクトリの`commondir`が、実際の`.git`ディレクトリを指しているためです。
### 脅威モデル / 前提条件
- 攻撃者は、GitPythonを使用して被害者が開いたりクローンしたりするリポジトリの内容を制御します(パブリックリポジトリ、フォークされたプルリクエスト、ミラーされた依存関係)。
- コード実行を行うには、被害者はGitPythonのネイティブな`index.commit()`を使用してコミットを実行する必要があります。ファイル読み込みの影響については、リポジトリを開いて設定ファイルを読み取るだけで十分です。
- `GIT_WORK_TREE`は**有効な対策ではありません**。`git_dir`が割り当てられ、環境が参照される前に探索処理が中断されます。
- 実際のGitには影響はありません。GitPythonのみがディレクトリを誤って解決します。
### 対策
1. `gitdir`/`commondir`/`HEAD`のトリプレットと、`is_git_dir(curpath)`の前に、`curpath/.git`をテストします。
2. `hook_path` / `_commit_hook_path` / `_get_validated_reflog_path`を`repo.common_dir`を通じて解決します。
3. `commondir`/`gitdir`の内容を結合する前に、包含チェックを行います(`SymbolicReference._get_validated_path`を再利用)。
4. `is_git_dir`で`HEAD`を検証します(`ref: refs/...`または40文字の16進数SHAを要求)。
5. `Repo._config_reader`で`merge_includes=False`を渡します(`git/repo/base.py:765`)。
### メンテナ向けの注意
コミット`406b98e1`(2026年5月31日、「コミットフックに対してcore.hooksPathを尊重する」)は、`git rev-parse --git-path`を使用してフックパスを解決しましたが、これは実際の`.git`を再発見するため、有効です。コミット`9bc287a2`(2026年7月20日)は、無条件の`rev-parse`への依存を避けるためにこれを元に戻し、実行ステップを再導入しました。
健全なレイアウトで各コミットで測定しました:`406b98e1` → フックは発火しませんでした。`9bc287a2`… `3.1.59` → フックが発火しました。
### 範囲に関する注意
報告されているのは、**リポジトリのルート**の場合のみです。実際の`.git`が存在する場合、Gitはそれを優先し、GitPythonも同様です。サブディレクトリにある偽のgitディレクトリは、実際のGitも騙されるため、このバリアントは、GitPythonの欠陥というよりも、一般的なエコシステムのリスクの範囲外です。
### 実証ファイル:
[poc1_rce.py](https://github.com/user-attachments/files/31388464/poc1_rce.py)
[poc2_file_read.py](https://github.com/user-attachments/files/31388465/poc2_file_read.py)
影響を受けるパッケージ:
- pip (Python) gitpython: 影響を受けるバージョンは 3.1.59 以下、修正されたバージョンは 3.1.60。