GitPythonの`parse_actor_and_date()`関数は、不正な形式のID行に対して、二次的なバックトラックを引き起こす脆弱性があります。
### 概要
これは、GHSA-g5vv-9gxw-82hx / CVE-2026-87819の残存問題です。バージョン3.1.60の修正で、`Actor._from_string`関数から正規表現の使用が削除されましたが、`parse_actor_and_date`関数は、依然として`_re_actor_epoch`パターンを、著作者/コミッター/タグ付け者の行の、一つ前の段階で実行しています。このパターンは、末尾のepoch(タイムスタンプ)またはタイムゾーン情報が欠落している、または不正な形式である場合に、二次的にバックトラックするため、同じ種類の遅い処理が、バージョン3.1.62でも発生する可能性があります。
### 詳細
`git/objects/util.py:322`:
```python
_re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")
```
`.+?`と`(.*)`は、どちらも同じ数のスペースを消費する可能性があり、必須の` (\d+) ([+-]\d+)`という末尾部分のチェックは、それらの後にのみ行われます。有効な末尾部分がない長い行では、検索はすべての分割ポイントを試行します。
この関数は、`parse_actor_and_date`関数(`git/objects/util.py:335`)から呼び出され、この関数は、`Commit._deserialize`関数(`git/objects/commit.py:917`、`:931`)の著作者およびコミッターの行、および`git/objects/tag.py:129`のタグ付け者の行で実行されます。
公開されているペイロード(文字と未終了の`<`)は、バージョン3.1.62では高速に処理されます。文字をスペースに置き換えることは、高速ではありません。
| author行 (authorの後の部分) | 2 KB | 8 KB | 32 KB |
|---|---|---|---|
| `'a'*n + ' <'` (アドバイザリペイロード) | 0.0 ms | 0.1 ms | 0.4 ms |
| `' '*n` | 28 ms | 453 ms | 7,700 ms |
| 有効な行、長い名前 (コントロール) | 0.0 ms | 0.0 ms | 0.0 ms |
処理時間は、長さが4倍になるごとに約16倍増加します。
### PoC (概念実証)
```python
import time
from git.objects.util import parse_actor_and_date
line = "author " + " " * 32000
t = time.perf_counter()
parse_actor_and_date(line)
print(f"{time.perf_counter() - t:.1f} s") # 3.1.62では約7秒
```
全体として、32 KBのスペースで構成され、不正な末尾部分(epochがない、タイムゾーンがない、数値ではないepoch、または不正なタイムゾーン)を持つコミットオブジェクトの`Commit.author`を読み取るには、約7.2秒かかります。同じ行に有効な`1191999972 -0700`という末尾部分がある場合は、1.3 msで処理されます。
再現に関する注意: `git hash-object -t commit -w` は、そのようなオブジェクト(`missingEmail`)を拒否します。そのため、テストオブジェクトは `--literally` オプションを使用して作成されました。リモートリポジトリは依然としてそのようなオブジェクトを提供できます。これは、`fetch.fsckObjects` と `transfer.fsckObjects` がデフォルトで false に設定されているためです。これは、CVE-2026-87819(「不正な作成者フィールドを持つコミットオブジェクトを作成する」)と同様の仕組みです。
### 影響
信頼できないリポジトリからコミットやタグを読み取るために GitPython を使用するアプリケーションにおいて、CPU リソースの枯渇(CWE-1333)が発生する可能性があります。具体的には、CI ツール、リポジトリスキャナ、コードホスティング連携などが該当します。作成されたオブジェクトに対して `Commit.author`、`Commit.committer`、または `TagObject.tagger` にアクセスするたびに、数秒かかる場合があります。また、リポジトリにはそのようなオブジェクトが多数含まれている可能性があります。
### 修正案
先頭のフィールド名を `.[^ ]*` でマッチさせるように変更します。これは、`.+?` ではなく、最初の文字の後の最初のスペースで停止します。ただし、`.[^ ]*` は、続く `(.*)` と重なることができないため、バックトラックは解消されます。
```python
_re_actor_epoch = re.compile(r"^.[^ ]* (.*) (\d+) ([+-]\d+).*$")
_re_only_actor = re.compile(r"^.[^ ]* (.*)$")
```
現在の main ブランチ(73a72c9)での検証結果:
- 368 行の作成者情報に関する差分(実世界のデータとエッジケースを含む):どちらの正規表現でも、マッチグループに 0 件の違いはありません。
- 64 KB の `' '*n` データ:修正前は 32,170 ms、修正後は 0.98 ms。有効な行は変更されていません(0.01 ms)。
- 既存のテストスイートでは、修正の有無にかかわらず、同じパス/失敗の結果が得られます。
- 新しい回帰テスト `test_parse_actor_and_date_malformed_line_is_linear` は、古いパターンでは失敗(8.4 秒)、修正後のパターンでは成功(0.5 秒)します。
完全な修正パッチ(修正と回帰テストを含む)は、以下に添付されています。
```diff
diff --git a/git/objects/util.py b/git/objects/util.py
index a68d701f..d8c0594d 100644
--- a/git/objects/util.py
+++ b/git/objects/util.py
@@ -319,8 +319,11 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]:
# 事前コンパイルされた正規表現
-_re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")
-_re_only_actor = re.compile(r"^.+? (.*)$")
+# 先頭のフィールド名("author", "committer", "tagger")は、".+?" ではなく ``.[^ ]*`` でマッチさせます。
+# 両方とも最初のスペースで止まりますが、``.[^ ]*`` は ``(.*)`` と重なることがないため、
+# 有効な末尾のepoch/offsetがない行が、二次関数的にバックトラックするのを防ぎます。
+_re_actor_epoch = re.compile(r"^.[^ ]* (.*) (\d+) ([+-]\d+).*$")
+_re_only_actor = re.compile(r"^.[^ ]* (.*)$")
def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]:
diff --git a/test/test_util.py b/test/test_util.py
index cf4299d5..e1631420 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -22,6 +22,7 @@ from git.cmd import dashify
from git.objects.util import (
altz_to_utctz_str,
from_timestamp,
+ parse_actor_and_date,
parse_date,
tzoffset,
utctz_to_altz,
@@ -571,6 +572,30 @@ class TestUtils(TestBase):
Actor("name last another", "some-very-long-email@example.com"),
)
+ def test_parse_actor_and_date_malformed_line_is_linear(self):
+ # 末尾のepoch/offsetが欠落しているか、不正な長いauthor行は、
+ # _re_actor_epoch が二次関数的にバックトラックする原因となっていました。
+ # 32 KBの行に対して、Commit.authorへのアクセスで ~7 秒かかっていました。
+ # 解析結果は変更されないはずですが、それまでに費やされる時間は変化する可能性があります。
+ for tail in ("", " notanumber -0700", " 1191999972", " -0700", " 1191999972 xx"):
+ line = "author " + " " * 32000 + tail
+ start = time.perf_counter()
+ actor, epoch, offset = parse_actor_and_date(line)
+ self.assertLess(time.perf_counter() - start, 1.0)
+
```
self.assertEqual(epoch, 0)
+ self.assertEqual(offset, 0)
+
+ # 正しくフォーマットされた行、長い名前を含むものも、以前と同じように解析されます。
+ actor, epoch, offset = parse_actor_and_date(
+ "author Tom Preston-Werner <tom@mojombo.com> 1191999972 -0700"
+ )
+ self.assertEqual(actor, Actor("Tom Preston-Werner", "tom@mojombo.com"))
+ self.assertEqual(epoch, 1191999972)
+ self.assertEqual(offset, 25200)
+
+ actor, epoch, offset = parse_actor_and_date("author " + "a " * 16000 + "<a@b> 1 -0700")
+ self.assertEqual(actor.email, "a@b")
+ self.assertEqual(epoch, 1)
+
@ddt.data(
("name", ""),
("name", "prefix_"),