MONAILabelサーバーのクラッシュと、作成されたDICOMファイルによる医療AI推論結果のサイレントな改ざん(pydicom DS/IS検証のバイパス)

### 概要

認証されていないリモート攻撃者は、REST APIを通じて単一の作成されたDICOMファイル(約132 KB)をアップロードすることで、実行中のMONAILabelサーバーをクラッシュさせたり、その医療AI推論結果をサイレントに改ざんしたりする可能性があります。この脆弱性は、pydicomがDecimal String (DS)およびInteger String (IS)のDICOM値表現に対する検証を欠いていることに起因し、NaN(非数)および無限大の値がMONAIのPydicomReaderを通過し、空間変換および機械学習推論パイプライン全体に伝播する可能性があります。

デフォルトのMONAILabel REST APIには認証は不要です。攻撃者は、サーバーへのネットワークアクセス(デフォルトポート8000)のみが必要です。

**深刻度:** CVSS 3.1: 8.7 HIGH
**CVSSベクトル:** CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:L
**CWE:** CWE-20 (不適切な入力検証) → CWE-682 (誤った計算)

### 詳細

**根本原因 — レイヤー1: pydicomが無効なVR値を許容する**

pydicomの`DSfloat.__new__()`は、Pythonの`float(val)`を呼び出しますが、これは「NaN」、「inf」、および「-inf」をサイレントに許容します。DICOM規格(PS3.5 表6.2-1)では、DSは0〜9の数字、+、-、.、eのみを定義しています。NaNとinfは有効なDS値ではありません。`is_valid_ds()`チェックは、RAISE検証モードでのみ実行され、これはデフォルトではありません。デフォルトモードはWARNであり、それでも無効な値を返します。

```python
>>> from pydicom.valuerep import DSfloat
>>> DSfloat("NaN")
nan # サイレントに許容される — 拒否されるべき
>>> DSfloat("inf")
inf # サイレントに許容される — 拒否されるべき
```

これは、0.9.7から3.0.2までのすべてのpydicomバージョンに影響します。

関連するバイパスは、pydicomのISタイプにも存在します。`IS("1e4")`は、`int(float(val))`のフォールバックによって10000を返しますが、DICOM規格で有効でない科学表記を許容します。

**根本原因 — レイヤー2: MONAI Coreがpydicomの出力を検証せずに信頼する**

MONAIの`PydicomReader._get_affine()`関数は、PixelSpacing、ImagePositionPatient、ImageOrientationPatientから4x4のAffine変換行列を構築しますが、`isfinite()`または`isnan()`のチェックは行われません。PixelSpacingにNaNが含まれていると、Affine行列の対角成分にNaNが生成され、すべての空間変換に影響を与えます。
`PydicomReader._get_affine() → LoadImaged → Spacingd → Model → Writer`

**根本原因 — レイヤー3: MONAILabel REST APIはあらゆるものを許容する**

MONAILabelの`PUT /datastore/`エンドポイントは、認証なし(デフォルト設定)、空間メタデータの検証なし、およびDICOMタグの値に対する範囲チェックなしで、DICOMファイルをアップロードできます。悪意のあるDICOMファイルは永続的に保存され、推論に使用できるようになります。

### 実証

**環境構築:**
```bash
# MONAILabelを起動 (Docker)
docker pull projectmonai/monailabel:latest
docker run -d --name monailabel-poc -p 8000:8000 --memory=8g \
projectmonai/monailabel:latest monailabel start_server \
--app /opt/monai/sample-apps/radiology \
--studies /opt/monai/datasets/Task09_Spleen/imagesTr \
--conf models segmentation

# サーバーが起動しているか確認
curl http://localhost:8000/info/
```

**ステップ1 — ベーラインの確立 (正常なデータが動作することを確認):**
```bash
curl -X POST "http://localhost:8000/infer/segmentation?image=spleen_40" \
-o baseline.nii.gz -w "HTTP %{http_code} - %{size_download} bytes"
# 結果: HTTP 200, 約223,628バイトの有効なセグメンテーションマスク
```

**ステップ2 — 攻撃の実行 (NaNを含むDICOMファイルのアップロード + 推論):**
```bash
pip install pydicom numpy requests
python poc_monailabel_ds_bypass.py --server http://localhost:8000
```

PoCスクリプトの内容:
1. pydicomがDSフィールドにNaN/infを受け入れることを確認します。
2. 悪意のあるDICOMファイルを3つ作成します(それぞれ約132KB)。これらのファイルには、PixelSpacingとImagePositionPatientにNaN/infが含まれています。
3. 各ファイルを`PUT /datastore/`経由でアップロードします(認証は不要)。HTTP 200が返されます。
4. `POST /infer/segmentation`経由で推論をトリガーします。HTTP 500エラーが発生し、サーバーがクラッシュします。
**実証された結果:**

| データ | アップロード | 推論 | 結果 |
|---|---|---|---|
| 基本設定 (spleen_40) | 該当なし | HTTP 200 | 有効な 223 KB のセグメンテーション |
| NaN PixelSpacing | HTTP 200 | HTTP 500 | RuntimeError によるクラッシュ (18.0 秒の計算リソースが無駄に消費) |
| NaN PixelSpacing + IPP | HTTP 200 | HTTP 500 | RuntimeError によるクラッシュ (16.7 秒の計算リソースが無駄に消費) |
| Inf PixelSpacing | HTTP 200 | HTTP 500 | RuntimeError によるクラッシュ (16.4 秒の計算リソースが無駄に消費) |

**サーバークラッシュのエラーメッセージ:**
```
monailabel/tasks/infer/basic_infer.py → self.writer(data)
monailabel/transform/writer.py → write_itk(...)
itk/ITKCommonBasePython.py → itkProcessObject_Update()
RuntimeError: itkGDCMImageIO.cxx:1226:
ITK ERROR: GDCMImageIO: 浮動小数点数のバッファが渡されましたが、格納されるピクセルタイプが指定されていません。
```

各リクエストで、サーバーはクラッシュする前に約 16 秒の計算リソースを消費します (完全な機械学習推論パイプライン)。 NaN の DICOM データはデータストアに残っており、その画像 ID に対する将来の推論を行うと、サーバーが再びクラッシュします。

**ライブラリレベルでの確認:** 17/17 のテストベクトルが、MONAI 1.5.2 および pydicom 3.0.2 に対して確認されました。また、実際の MONAI の `LoadImaged` および `Spacingd` トランスフォームが、エラーなしで NaN のジオメトリを受け入れることも確認されました。

### 影響

1. **サービス拒否攻撃:** 約132KBのDICOMファイルをアップロードすると、MONAI Labelの推論パイプラインがクラッシュします。正当なユーザーからのすべての推論リクエストが中断されます。最小限の帯域幅で、この状況は何度でも再現可能です。
2. **サイレントなデータ破損:** 空間メタデータ内のNaN値が、推論パイプライン全体に伝播し、幾何学的に誤ったセグメンテーションマスクを生成します。出力書き込み機能がクラッシュしない場合、結果はHTTP 200ステータスで、警告なしに返されます。
3. **永続的なデータストア汚染:** 悪意のあるDICOMファイルは、`PUT /datastore/`を介して永続的に保存されます。その画像IDに対する将来の推論リクエストは、クラッシュまたは破損を引き起こし、サーバーの再起動後もその状態が維持されます。
**影響を受けるMONAIエコシステム (9つのリポジトリ):**
- MONAI Core: PydicomReader、LoadImaged、Spacingd変換
- MONAILabel: REST API + DICOMwebデータストア
- MONAI Deploy App SDK: DICOMDataLoaderOperator
- model-zoo、VISTA、GenerativeModels、チュートリアル、研究貢献
**影響を受けるDICOMタグ (すべてのDS VR):** PixelSpacing、ImagePositionPatient、SliceThickness、ImageOrientationPatient、RescaleSlope、WindowCenter/WindowWidth

**推奨される修正:**
1. `PydicomReader._get_affine()`において、PixelSpacing、IPP、IOPからのすべての値に対して`math.isfinite()`による検証を追加します。NaN/inf値が見つかった場合は、ValueErrorを発生させます。
2. MONAILabelの`PUT /datastore/`でアップロードされたDICOMの空間メタデータを検証します。無効な幾何構造の場合は、HTTP 400ステータスを返します。
3. `LoadImaged`/`Spacingd`でリサンプリングを行う前に、空間メタデータが有限の値であるか検証します。
4. pydicomのアップストリームと連携し、すべての検証モードにおいてDSフィールド内のNaN/inf値を拒否するようにします。
**アップストリーム:** pydicomのGHSAが2026年4月5日に報告されました。

**著者:** Paulino Calderon (paulino@calderonpale.com)

影響を受けるパッケージ:
- pip (Python) monai: 1.6.1より前のバージョンが影響を受けます。1.6.1で修正されました。