PhpSpreadsheet ODS リーダー:無制限の行の繰り返し → CPUとメモリの枯渇(DoS攻撃)

## 概要

PhpSpreadsheetがOpenDocumentスプレッドシート(`.ods`)を読み込む際、`Ods::processTableRow()`関数は、`<table:table-row>`要素の`table:number-rows-repeated`属性を、型変換(`(int)`)のチェックなしで読み込み、その値に基づいて行を繰り返し処理します。この値は**完全に無制限**であり、そのため、**わずか約1.4KBのファイル**でも、リーダーに**数百万のセル**を作成させることができ、メモリを枯渇させる(致命的なOOMエラー)可能性があります。認証や特別な設定、デフォルトの読み込みフィルタは不要です。ユーザーが提供した`.ods`ファイルを読み込むアプリケーションはすべて影響を受けます。

---

## 影響を受けるバージョン

`number-rows-repeated`の機能は、ODSリーダーの初期から存在しており(現在のリリースブランチ以前)、したがって、すべてのサポート対象バージョンが影響を受けます。

| ブランチ | 影響を受けるバージョン |
|--------|----------|
| 1.x | ≤ 1.30.6 |
| 2.0–2.1 | ≤ 2.1.18 |
| 2.2–2.4 | ≤ 2.4.7 |
| 3.x | ≤ 3.10.7 |
| 4.x–5.x | ≤ 5.9.0 (最新) |

---

## 根本原因(ソース → シンク)

`src/PhpSpreadsheet/Reader/Ods.php`:

```php
// 816行目 — 行の繰り返し処理、型変換のチェックなし
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
...
// 1151行目 — 行の生成ループ:上限なし
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
$rID = $rowID + $rowAdjust;
$cell = $spreadsheet->getActiveSheet()->getCell($columnID . $rID); // Cellオブジェクトを割り当て
$cell->setValueExplicit($dataValue, $type);
// ... $rowAdjustまたは$rIDに対する上限チェックはなし ...
}
```

**なぜ列は脆弱ではないか(すでに上限がある):**

1326行目の列ヘッダーのループでは、正しいパターンで上限チェックが行われています。

```php
// 1326行目 (processTableColumnHeader): BOUNDED が正しく機能
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
// HAS BOUNDS CHECK: && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT
}
```

さらに、1141行目のセルデータ列のループで `getCell()` が呼び出され、列が `MAX_COLUMN_INT` (XFD / 16,384) を超えると、`Coordinate.php` の 448 行目で例外が発生します。

```php
// Coordinate.php 448行目
if ($columnIndex > self::MAX_COLUMN_INT) {
throw new Exception('列の文字列インデックスは XFD を超えることはできません');
}
```

**結果:** 列は 16,384 に制限されます。行数には制限がありません。

---

## 影響

**メモリ不足 (単一の値を持つセル、多くの行の繰り返し):**
null ではないセル値と、非常に大きな `number-rows-repeated` 属性があると、数百万の `Cell` オブジェクトが生成され、PHP で `Allowed memory size ... exhausted` という致命的なエラーが発生します。

**例:**
- ペイロード: `<table:table-row table:number-rows-repeated="5000000">` (500万行)
- ファイルサイズ: 1.4 KB
- 消費メモリ: 約 256 MB (1行ごとに 1 つの `Cell` オブジェクト)
- 結果: デフォルトのメモリ制限を超えて OOM (Out of Memory) が発生

---

## 再現方法

**脆弱なペイロード (OOM を引き起こす):**

```xml
<table:table table:name="Sheet1">
<table:table-row table:number-rows-repeated="5000000">
<table:table-cell office:value-type="float" office:value="1">
<text:p>1</text:p>
</table:table-cell>
</table:table-row>
</table:table>
```

**脆弱ではないペイロード (例外を発生させる - 参照用):**

```xml
<table:table table:name="Sheet1">
<table:table-row>
<table:table-cell office:value-type="float" office:value="1"
table:number-columns-repeated="1000000000">
<text:p>1</text:p>
</table:table-cell>
</table:table-row>
</table:table>
```

この列の繰り返しに関する例では、`Column string index can not be beyond XFD` という例外が発生します (境界チェックによる防御策が既に実装されています)。

影響を受けるパッケージ:
- Composer (PHP) phpoffice/phpspreadsheet: 影響を受けるバージョンは 4.0.0 以降、5.9.0 以下。修正は 5.10.0 で行われました。
- Composer (PHP) phpoffice/phpspreadsheet: 影響を受けるバージョンは 3.3.0 以降、3.10.7 以下。修正は 3.10.8 で行われました。
- Composer (PHP) phpoffice/phpspreadsheet: 影響を受けるバージョンは 2.2.0 以降、2.4.7 以下。修正は 2.4.8 で行われました。
- Composer (PHP) phpoffice/phpspreadsheet: 影響を受けるバージョンは 2.0.0 以降、2.1.18 以下。修正は 2.1.19 で行われました。
- Composer (PHP) phpoffice/phpspreadsheet: 影響を受けるバージョンは 1.30.6 以下。修正は 1.30.7 で行われました。