datamodel-code-generator: Protobufの弱いインポート機能におけるパスの脆弱性により、ファイルが一時ディレクトリ以外の場所に書き込まれる可能性がある

## 概要

攻撃者が制御するProtobufスキーマを処理する際、`datamodel-code-generator`の脆弱なバージョンでは、生成された弱いインポートのスタブファイルが、意図された`__weak_imports__`という一時ディレクトリ以外の場所に書き込まれる可能性がありました。絶対パスや、`..`を含む相対パスを使用することで、このディレクトリを回避することができました。

攻撃者は、プロセスが書き込み可能な場所にディレクトリや新しいファイルを作成することができました。相対パスのトラバーサルにより、既存の書き込み可能なファイルが上書きされる可能性もありました。これは、ファイルの存在確認とスタブの書き込みが、異なるベースディレクトリを使用していたことが原因です。既存の絶対パスを持つターゲットファイルは、存在確認の際にスキップされていました。

書き込まれる内容は、生成されたProtobufの構文宣言(`syntax = "proto2";`または`syntax = "proto3";`)と、それに続く改行文字に限られていました。この問題は、攻撃者が完全に制御できるファイルの内容を提供するものではなく、直接的な任意のコード実行は確認されていません。

これらのファイルシステムへの影響は、`protoc`が実行される前に発生しました。その後のコンパイルエラーによって、これらの影響が解消されることはありません。この問題が発生するためには、攻撃者が制御するスキーマを、ユーザーまたはCIジョブが処理する必要があります。スキーマは、ローカルファイルとして提供されるか、`--url`オプションで取得されるかのいずれかです。

この問題に対する修正は、親リポジトリの`main`ブランチに[5e94b8f](https://github.com/datamodel-code-generator/datamodel-code-generator/commit/5e94b8f4203198798ec66b8e48217f70af69a0dc)でマージされました。修正されたパッケージのリリースはまだありません。
以下に示される技術的な詳細とPoCは、修正前の脆弱な実装について説明しています。

## 影響を受けるコンポーネント

- リポジトリ: `datamodel-code-generator/datamodel-code-generator` (以前は `koxudaxi/datamodel-code-generator`)。
- 分析対象: リリース版 **0.80.0** (`pip install datamodel-code-generator==0.80.0`) および `main` ブランチ、コミット `834731d56c0a90c182a0919f316069cf5ff0659a` (2026-09-13)。両方とも、同一の脆弱な箇所を含んでいました。
- 脆弱な箇所 / エントリーポイント (`src/datamodel_code_generator/parser/protobuf.py`、行番号は 0.80.0 のもの):
- ソース: `WEAK_IMPORT_PATTERN` (`protobuf.py:37`) — 攻撃者によって制御されるキャプチャグループ。
- 脆弱な箇所: `_write_missing_weak_imports` (`protobuf.py:391-393`) — `weak_import_dir / import_path` + `mkdir(parents=True)` + `write_text`。
- 実行タイミング: `_ProtoInputPreparer.__enter__` (`protobuf.py:333`) から呼び出され、`with` ブロック内で `protoc.main(...)` が実行される前に実行されます。
- 前提条件: 攻撃対象のシステムで、ツール(CLIまたはPython API)が `--input-file-type protobuf` オプションとともに実行され、攻撃者によって制御される `.proto` ファイルが、ローカルファイル (`--input`) またはリモートURL (`--url`) として渡される必要があります。 `grpcio-tools` パッケージ(Protobufサポート)が必要です。 攻撃者は、影響を受けるシステムに対する特権を必要としません。 ファイルシステムへの影響は、コード生成プロセスによって書き込み可能な場所に限定されます。

## 深刻度

提案: **高**。 `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N` = **7.5**。

- この脆弱性は、関連するパス・トラバーサルに関するアドバイザリであるCVE-2026-55389 / CVE-2026-55390と同じ評価基準に基づいて評価されています。その評価は、`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`で、スコアは7.5です。この脆弱性は、同じ攻撃対象領域(処理されたスキーマからの検証されていないパス)を持ちますが、影響が機密性(読み込み)から**完全性(書き込み)**へと変化しているため、`C:N/I:H`となっています。
- `AV:N`: 悪意のあるスキーマは、`--url`オプションを通じてネットワーク経由で取得可能です。これは、関連する脆弱性の評価方法と一致しています。
- `I:H`: 攻撃者は、書き込みパスを完全に制御でき、任意のディレクトリツリーを作成したり、既存のファイルを上書きしたりすることができます。ただし、ファイルの内容は制限されています(固定された`syntax = "proto{2,3}";`という行)。
- `A:N`は、保守的な選択です。ビルドに不可欠なファイルを上書きすると、可用性が失われる可能性がありますが、その場合、`A:L`と評価される可能性があります。
- より適切な解釈としては、「被害者はツールをファイル上で実行する必要がある」という点をユーザーインタラクション(`UI:R`)とみなした場合、スコアは**6.5(中程度)**になります。ただし、関連する読み込みパスの脆弱性については、プロジェクト側が`UI:N`を使用しているため、ここでは一貫性を持たせるために`UI:N`を使用しています。

## 詳細

ソースパターン:インポートされるパスは、文字の制限なしで取得されます。

```python
# protobuf.py:37
WEAK_IMPORT_PATTERN = re.compile(r'^\s*import\s+weak\s+"([^"]+)"\s*;', re.MULTILINE)
```

シンク:取得されたパスは結合され、境界チェックなしで書き込まれます。

```python
# protobuf.py:383-393 (_write_missing_weak_imports)
for import_path in WEAK_IMPORT_PATTERN.findall(text):
if any((include_path / import_path).exists() for include_path in include_paths):
continue
stub = self.weak_import_dir / import_path # no resolve(), no is_relative_to()
stub.parent.mkdir(parents=True, exist_ok=True) # arbitrary directory tree
stub.write_text(syntax, encoding=self.parser.encoding)
```

エスケープメカニズムは2つあり、どちらも`pathlib`の`/`のセマンティクスに由来します。

- **絶対パス**: `Path(".../__weak_imports__") / "/etc/x"` は `/etc/x` になります(絶対パスの右オペランドはベースを置き換えます)。プロセスが書き込める場所であれば、どこにでも新しいファイルを作成します。
- **`../` によるパスの移動**: `Path(".../__weak_imports__") / "../../x"` は、一時ディレクトリから上位のディレクトリに移動します。

存在確認のガード(389行目)が書き込みを保護しない理由は、`include_path / import_path` をチェックしているからです。このチェックでは、実際の書き込みで使用される `self.weak_import_dir / import_path` とは**異なるベース** (`include_paths`) が使用されています。相対パスの場合、この2つのベースは異なるディレクトリを指すため、ガードは実際の書き込み対象ではない場所をチェックし、それを逃してしまうことになり、**既存のファイルの上書きを可能にしてしまいます**。(絶対パスの場合、ガードと書き込みが一致するため、既に存在する絶対パスのターゲットはスキップされます。したがって、絶対パスは新しいファイルを作成しますが、上書きはしません。上書きは、相対パスによる移動によって実現されます。)

タイミング:`_write_missing_weak_imports` は `__enter__` (333行目) の中で実行され、`protoc.main(...)` は `with` ブロックの後半で実行されます。`protoc` は、不正な仮想パス(「バックスラッシュ、連続したスラッシュ、"."、".." は仮想パスで使用できません」)を検出し、全体としてコマンドがエラーで終了します。しかし、ファイルは既に書き込まれており、ファイルシステムの副作用はロールバックされません。

### 攻撃経路

1. 攻撃者は、例えば `import weak "/home/victim/.config/x";` や `import weak "../../overwrite_me.txt";` を含む `evil.proto` を作成し、被害者にそれを処理させます(サードパーティのスキーマ、または攻撃者がホストするファイルへの `--url`)。
2. `_write_missing_weak_imports` は、攻撃者が指定したパスにスタブファイルを書き込み、必要な中間ディレクトリを作成し、既存のファイルを上書きします(相対パスの場合)。
3. その後、`protoc` が失敗しますが、ファイルは既にディスク上に存在します。

## 実証

`datamodel-code-generator==0.80.0`がリリースされたバージョンを使用し、`--input`オプションには`poc/reproduce.sh`、`--url`オプションには`poc/reproduce-url.sh`という自己完結型のスクリプトが用意されています。これらのスクリプトは、悪意のある`.proto`ファイルを配信/参照し、書き込み操作を実行します。PoCは、**リリースされた**パッケージをそのまま使用し、書き込まれたファイルのSHA256ハッシュ値は`REPRODUCE.md`に記録されています。

0.80.0バージョンに対するローカルでの`--input`オプションを使用した実行結果(実際の出力):

```
[*] インストールされたバージョン: 0.80.0
[テストA] 絶対パスを使用した任意のファイル作成とディレクトリ作成:
[PASS] 攻撃者が制御する絶対パスに新しいファイルが作成されました
[PASS] コンテンツは固定された構文のスタブです
[テストB] 相対パスを使用した既存のファイルの上書き:
[PASS] 既存のvictim.txtの内容が上書きされました
[PASS] victim.txtには、挿入されたスタブが含まれるようになりました
==================== 結果: 4件成功、0件失敗 ====================
```

0.80.0バージョンに対するリモートでの`--url`オプションを使用した実行結果(実際の出力)(HTTP経由で`.proto`ファイルが配信され、その後、同様の書き込み操作が実行されます):

```
[*] 悪意のある`.proto`ファイルをhttp://127.0.0.1:<port>/evil_url.protoで配信しています
[PASS] サーバーが実際に`.proto`ファイルを配信しました(HTTP 200)
[PASS] 絶対パスを使用した任意のファイルが作成されました
[PASS] 相対パス(../)を使用して、既存のファイルを上書きしました
==================== 結果: 3件成功、0件失敗 ====================
```

## 影響

`datamodel-code-generator`の脆弱なバージョンを使用し、信頼できないProtobufスキーマ(ダウンロードしたサードパーティ製の`.proto`ファイル、またはリモートソースへの`--url`オプション)を使用するユーザーまたはCIパイプラインは、スキーマの作成者が、プロセスの自身の権限で、任意のファイル/ディレクトリの作成と上書きを受ける可能性があります。具体的な被害:

- ソースコード、設定ファイル、`.env`ファイル、ロックファイル、または`__init__.py`ファイル/エントリーポイントファイルを上書きすることで、プロジェクトの破損、ビルドの失敗、サービス拒否を引き起こす可能性があります。
- 任意のディレクトリツリーを作成したり、監視対象または自動ロードされる場所にファイルを配置したりする可能性があります。

任意のコードの直接的な実行は確認されていません。書き込まれる内容は、生成された`syntax = "proto2";`または`syntax = "proto3";`という行に限られており、攻撃者が完全に制御できるものではありません。

## 対策

この修正は、親リポジトリの `main` ブランチに、[5e94b8f](https://github.com/datamodel-code-generator/datamodel-code-generator/commit/5e94b8f4203198798ec66b8e48217f70af69a0dc) で統合されました。

Protobuf の入力準備機能は、専用のテンポラリディレクトリと、各候補となるスタブファイルのパスを解決するようになりました。解決された候補がそのディレクトリ外にある場合、インクルードパスの確認、親ディレクトリの作成、またはスタブの書き込みを行う前に、`SchemaParseError` が発生します。このチェックにより、サンドボックスから抜け出す可能性のある絶対パスのエスケープや、相対パスの `..` による移動が拒否されます。

入力準備の失敗が発生した場合、テンポラリディレクトリは直ちに削除されます。

回帰テストでは、以下の内容が検証されています。

- サンドボックス外にある絶対パスの weak-import の拒否。
- サンドボックス外にある相対パスの `..` による移動の拒否。
- サンドボックス外にある既存ファイルの保持。

ローカル環境での tox による検証は、Python 3.10~3.14 およびフォーマッタの組み合わせ、CI 環境、およびリポジトリのチェックで問題なく通過しました。変更されたファイルを含む、行とブランチの網羅率は 100% に達しました。

修正されたパッケージはまだ PyPI に公開されていません。この更新時点での最新の公開バージョンは 0.80.0 です。`Patched versions` は、この修正を含むバージョンがリリースされるまで、未設定のままです。このアドバイザリは、そのリリースが提供されるまで非公開のままとなります。

## 参照

- GHSA-8359-h9fx-j6v9 / CVE-2026-55389 — datamodel-code-generatorにおいて、JSONスキーマの`$ref`パストラバーサルを介した、任意のローカルファイルの**読み込み**が可能 (CWE-22/200/610、7.5、0.62.0で修正済み)。同じクラスで、プリミティブの読み込み; 関連するエントリーポイント。
- GHSA-442q-2j6p-642g / CVE-2026-55390 — datamodel-code-generatorにおいて、XSDの`schemaLocation`パストラバーサルを介した、任意のローカルファイルの**読み込み**が可能 (CWE-22/200/610、7.5、影響を受けるバージョンは`>= 0.59.0, <= 0.61.0`、0.62.0で修正済み)。同じクラスで、プリミティブの読み込み; Protobufのエントリーポイントを見逃した修正。
- Pythonの`pathlib` — `PurePath.__truediv__`: 絶対パスの右オペランドは左オペランドを無視し、`..`セグメントは正規化されない。(https://docs.python.org/3/library/pathlib.html)

## 発見

ソースコードのレビューと、`datamodel-code-generator/datamodel-code-generator` (バージョン0.80.0、`main`ブランチのコミットハッシュ@`834731d56c0a90c182a0919f316069cf5ff0659a`, 2026年9月) での再現可能なPoCによって発見されました。

影響を受けるパッケージ:
- pip (Python) datamodel-code-generator: 影響を受けるバージョンは`>= 0.59.0, <= 0.80.0`、0.81.0で修正済み。