MLflowにおいて、認証されていないサーバー上で、サードパーティ製のスコアのシリアライズ/デシリアライズ処理を通じて、リモートコード実行の脆弱性が存在する。これは、呼び出し元が指定したアーティファクトモジュールをインポートすることによって引き起こされる。

## 概要

MLflow 3.12.0では、OSS MLflowにおいて、サードパーティ製のスコアの登録とデシリアライズのサポートが導入された。スコアのデシリアライズ処理中、サーバーは、許可されたパッケージのプレフィックスのいずれか下の、リクエストによって制御されるPythonモジュールのパスを受け取り、そのモジュールをインポートし、リクエストによって制御されるクラスを取得し、それを同期的にインスタンス化する。

HTTPクライアントは、この動作をMLflowの組み込み実験、実行、およびアーティファクトアップロードのエンドポイントと組み合わせることができる。

1. ローカルアーティファクトの場所が、書き込み可能な許可されたスコアパッケージを指すように、実験を作成する。
2. 実行を作成し、MLflowが`<artifact_location>/<run_id>/artifacts/`を割り当てるようにする。
3. その実行アーティファクトディレクトリにPythonモジュールをアップロードする。
4. アップロードされたファイルへのパスが解決される、シリアライズされたサードパーティ製のスコアを送信する。
5. `/ajax-api/3.0/mlflow/scorer/invoke`を通じて、サーバー側のインポートとクラスの構築をトリガーする。

公式のMLflow v3.14.0イメージを使用したデフォルトの認証なしサーバーでは、これにより、MLflowサーバープロセスの権限で、認証されていない任意のPythonコードの実行が可能になる。再現されたイメージは、プロセスをuid 0で実行し、インストールされたPythonパッケージツリーへの書き込みを許可する。

添付の概念実証は、意図的に無害である。アップロードされたコンストラクタは、1つの操作のみを実行する。それは、自身のアップロードされた実行アーティファクトの横に、ランダムなマーカーを書き込むことである。そのマーカーは、MLflowの実行アーティファクトのエンドポイントを通じてダウンロードされ、正確な一致が確認される。

## 技術的な詳細

### 1. 許可リストは、攻撃者が作成した派生物を受け入れる

リリースコミット`86cd7f5a1dc25a1387ad07c87811bbc30f62951c`において、`mlflow/genai/scorers/scorer_utils.py`は、これらの許可されたルートを定義している。

```python
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
```

`mlflow/genai/scorers/base.py`において、デシリアライザは、完全一致するルート、またはその派生物のいずれかを受け入れます。

```python
module_path == m or module_path.startswith(m + ".")
```

その後、リクエストから提供された値を使用して、以下の操作が実行されます。

```python
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None)
scorer_instance = scorer_class(**init_kwargs)
```

パッケージ名のプレフィックスは、セキュリティ上の境界ではありません。サーバープロセスが許可されたパッケージ以下のディレクトリに書き込める場合、攻撃者が作成した派生物が文字列のプレフィックスのチェックを通過し、インポート可能なPythonコードとして扱われる可能性があります。

関連するソースコードの場所(v3.14.0リリース時のコミット):

* `mlflow/genai/scorers/scorer_utils.py`: 許可されたサードパーティ製評価モジュール
* `mlflow/genai/scorers/base.py`: サードパーティ製評価のデシリアライゼーションと構築

### 2. デシリアライゼーションはトレース処理の前に発生する

`mlflow/server/handlers.py`の`_invoke_scorer_handler()`は、提供されたシリアライズされた評価モジュールを解析し、以下の関数を呼び出します。

```python
scorer = Scorer.model_validate_json(serialized_scorer)
```

デシリアライゼーションと構築の後で、ハンドラは以下の関数を呼び出します。

```python
batches = get_trace_batches_for_scorer(trace_ids, scorer, tracking_store)
```

したがって、攻撃者が制御できるコンストラクタは、サーバーが提供されたトレースIDを検証または解決する前に実行されます。

最小限の検証用クラスは、意図的に完全なMLflow評価モジュールではありません。コンストラクタがマーカーを書き込んだ後、後続のトレースバッチ処理のロジックは、検証用クラスが実装していない評価モジュールの属性にアクセスします。その結果、HTTP 500エラーが発生しますが、これはコードの実行後であり、検証結果を無効にしません。

コンストラクタの実行には、バックグラウンドジョブの実行や特別なジョブ実行サーバーのオプションは必要ありません。

### 3. MLflowのアーティファクト配置における、信頼済みプレフィックスの使用

実験の作成エンドポイントは、`artifact_location`を受け取ります。ファイルベースの実験の場合、MLflowは各実行のアーティファクトディレクトリを以下のように定義します。

```text
<artifact_location>/<run_id>/artifacts/
```

この概念実証では、公式のv3.14.0イメージ内の以下のパッケージディレクトリが選択されます。

```text
file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens
```

`audit_<token>.py`をアップロードする前に実行を作成すると、結果として得られるファイルシステムの場所は以下のようになります。

```text
/usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens/<run_id>/artifacts/audit_<token>.py
```

対応するインポートパスは以下のようになります。

```text
mlflow.genai.scorers.trulens.<run_id>.artifacts.audit_<token>
```

この攻撃は、既存のパッケージモジュールを上書きしません。代わりに、通常のファイルベースのアーティファクト配置に従って、新しい実行スコープのモジュールを作成します。実行IDは、呼び出し元に返されます。

### 4. 認証と認可の動作

デフォルトの再現サーバー構成のように、認証が無効になっている場合、認証情報は不要です。

MLflowの基本的な認証が有効になっている場合でも、認証は適用されます。ただし、スコアの呼び出しを行うAJAXエンドポイントは、`validate_gateway_proxy`に割り当てられており、これは実験レベルまたはスコアレベルのリソース権限チェックを行わずに`True`を返します。

認証された環境での実用的な悪用には、呼び出し元が実験と実行を作成し、それらにアーティファクトをアップロードする必要があります。重要な認可の問題は、インポートが行われる前に、提供された実験に対するリソース固有の権限チェックによって、スコアのデシリアライゼーション自体が制限されていないことです。

## 概念実証

添付された再現環境は、意図的に分離されています。

* 正式なMLflow v3.14.0のイメージが、マニフェストのダイジェストによって固定されています。
* Dockerネットワークが無効になっています。
* MLflowはコンテナのループバックにバインドされています。
* シェルコマンドや外部コールバックはありません。
* 外部リクエストはありません。
* ホストファイルシステムの書き込みはありません。
* アップロードされた実行アーティファクトの横にマーカーが作成されます。
* マーカーは、実行アーティファクトのダウンロードエンドポイントを通じて取得されます。
* 一時的なコンテナは、完了後に削除されます。

実行方法:

```bash
./run-official-image.sh
```

期待される出力:

```text
MLFLOW_VERSION=3.14.0
PACKAGE_DIR=/usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens
CREATE_EXPERIMENT_STATUS=200
CREATE_RUN_STATUS=200
UPLOAD_STATUS=200
INVOKE_STATUS=500
PROOF_DOWNLOAD_STATUS=200
PROOF_MATCH=1
RESULT=AFFECTED
```

セキュリティに関わる結果は以下の通りです。

```text
PROOF_MATCH=1
RESULT=AFFECTED
```

これは、攻撃者が制御するコンストラクタが、後続のスコア処理の失敗が発生する前に、MLflowサーバープロセス内で実行されたことを証明しています。

## 再現環境

* **バージョン:** MLflow v3.14.0
* **リリースコミット:** `86cd7f5a1dc25a1387ad07c87811bbc30f62951c`
* **正式イメージ:** `ghcr.io/mlflow/mlflow:v3.14.0`
* **マニフェストダイジェスト:** `sha256:967de3ca34b6a5751718e380a0a383eb0ce1a6f5b484f363bd0da7559117a5f8`
* **Pythonバージョン:** 3.10.18
* **イメージに設定されたユーザー:** なし
* **観察された実行ユーザー:** uid 0
* **サーバーバインド:** `127.0.0.1:5000`
* **コンテナネットワーク:** 無効
* **バックエンドストア:** `sqlite:////work/mlflow.db`
* **デフォルトのアーティファクトルート:** `/work/artifacts`
* **認証:** 無効
* **特別なジョブ実行オプション:** なし

一時的なコンテナ内のサーバーコマンド:

```bash
mlflow server \
--host 127.0.0.1 \
--port 5000 \
--backend-store-uri sqlite:////work/mlflow.db \
--default-artifact-root /work/artifacts
```

## 影響範囲とソースの lineage

脆弱なサードパーティ製スコアのデシリアライゼーションの挙動は、以下のコミットによって導入されました。

```text
02eea26035b9d8e65af31e6e8f058ca195f62cdd
Enable third-party scorer registration in OSS MLflow (#22634)
```

v3.11.1ではこの機能は存在せず、v3.12.0で初めて登場しました。

v3.14.0で、この問題の再現に成功しました。関連する`mlflow/genai/scorers/base.py`の内容は、masterブランチのコミット時点では変更されていません。

```text
20858c9426a9644dfcf9545a92cbb534376779eb
```

2026年7月12日現在、修正された公式リリースは見つかっていません。

## 影響

再現された標準イメージにおいて、トラッキングサーバーにアクセスできるネットワーク経由からの呼び出し元は、認証情報やユーザーの操作なしに、MLflowプロセスとしてPythonコードを実行できます。

この脆弱性が悪用されると、MLflowサーバープロセスがアクセスできるすべてのデータ、認証情報、ネットワークアクセス、およびファイルシステム上の場所に、機密性、完全性、および可用性の問題が発生する可能性があります。

標準イメージの実行時のユーザーIDが0であるため、コンテナレベルでの影響が大きくなります。ホストへの影響は、コンテナランタイムの設定、マウントされたパス、付与された機能、ソケットの公開、およびその他の隔離制御に依存します。

認証が有効な環境では、この脆弱性を悪用するには、実験と実行を作成し、アーティファクトをアップロードできる認証されたユーザーが必要です。また、サーバープロセスが許可されたスコアパッケージのいずれかへの書き込み権限を持っている必要があります。この条件は、再現された公式イメージでは満たされており、同じオペレーティングシステムユーザーによってインストールおよび実行される仮想環境でも満たされる可能性があります。

読み取り専用のコンテナファイルシステム、または別のユーザーが所有し、書き込み権限がないスコアパッケージのディレクトリでは、この脆弱性の悪用は防がれます。

## アーティファクトと添付ファイル

### リモートのPoCスクリプト

PoCスクリプト:[new_remote_poc.py](https://github.com/user-attachments/files/29936616/new_remote_poc.py)

リモートのPoCは、以下の手順で実行されました。使い捨てマシンのパブリックIPアドレスは伏せられています。

```bash
python3 new_remote_poc.py \
--target http://<REDACTED_PUBLIC_IP> \
--package-dir /usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens \
--acknowledge-owned-target I_OWN_THIS_TARGET
```

完全なサニタイズされた出力は以下の通りです。

TARGET=http://<REDACTED_PUBLIC_IP>
VERSION_STATUS=200
MLFLOW_VERSION=3.14.0
CREATE_EXPERIMENT_STATUS=200
EXPERIMENT_ID=2
CREATE_RUN_STATUS=200
RUN_ID=9a2d665eb38940b68983f0586f623115
UPLOAD_STATUS=200
IMPORT_PATH=mlflow.genai.scorers.trulens.9a2d665eb38940b68983f0586f623115.artifacts.audit_1783843760683972103
INVOKE_STATUS=500
INVOKE_RESPONSE_PREVIEW='<!doctype html>\n<html lang=en>\n<title>500 Internal Server Error</title>\n<h1>Internal Server Error</h1>\n<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>\n'
PROOF_DOWNLOAD_STATUS=200
EXPECTED_MARKER_SHA256=7a423b479df3524567fdf8cc3e7f6c4bfccc577fbcc4bac9519db8bd1cc61ed5
PROOF_MATCH=1
SERVER_CLEANUP_PATH=/usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens/9a2d665eb38940b68983f0586f623115
PROOF_OUTPUT_BEGIN
{
"cwd": "/",
"machine": "x86_64",
"marker": "MLFLOW_REMOTE_SAFE_MARKER_1783843760683972103",
"os_release": "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n",
"platform": "Linux-7.0.0-1006-aws-x86_64-with-glibc2.31",
"python_implementation": "CPython",
"python_version": "3.10.18",
"server_datetime_utc": "2026-07-12T08:09:43.555055+00:00",
"whoami": "root",
"whoami_returncode": 0,
"whoami_stderr": ""
}
PROOF_OUTPUT_END
RESULT=AFFECTED
EXPERIMENT_METADATA_CLEANUP_STATUS=200
CLEANUP_NOTE=一時的なテストインスタンスを破棄するか、所有しているホストからSERVER_CLEANUP_PATHを削除してください。

検証ファイルは、使い捨てコンテナ内で直接確認されました。

```bash
cat /usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens/9a2d665eb38940b68983f0586f623115/artifacts/execution-proof-1783843760683972103.json
```

ファイルの内容は以下の通りです。

```json
{
"cwd": "/",
"machine": "x86_64",
"marker": "MLFLOW_REMOTE_SAFE_MARKER_1783843760683972103",
"os_release": "PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n",
"platform": "Linux-7.0.0-1006-aws-x86_64-with-glibc2.31",
"python_implementation": "CPython",
"python_version": "3.10.18",
"server_datetime_utc": "2026-07-12T08:09:43.555055+00:00",
"whoami": "root",
"whoami_returncode": 0,
"whoami_stderr": ""
}
```

結果は、以下のことを示しています。

* 攻撃者が制御するモジュールが、許可されたスコアリングパッケージの下にアップロードされました。
* MLflowは、リクエストで選択されたモジュールをインポートし、そのコンストラクタを呼び出しました。
* コンストラクタは、MLflowサーバープロセスとして実行され、再現された公式イメージでは`root`ユーザーとして実行されていました。
* コンストラクタは、ランダムなマーカーと実行時間に関する情報を含む、サーバー側の証明ファイルを作成しました。
* 証明ファイルは、MLflowのアーティファクトダウンロードエンドポイントを通じて正常に取得されました。
* HTTP 500エラーは、コンストラクタの実行後にのみ発生しました。

再現の指標は以下の通りです。

```text
UPLOAD_STATUS=200
PROOF_DOWNLOAD_STATUS=200
PROOF_MATCH=1
RESULT=AFFECTED
```

テスト後、一時的なクラウドマシンとコンテナは削除されました。

ローカルでの再現スクリプト:
[run-official-image.sh](https://github.com/user-attachments/files/29936514/run-official-image.sh)
[poc_local.py](https://github.com/user-attachments/files/29936506/poc_local.py)

影響を受けるパッケージ:
- pip (Python) mlflow: 影響あり >= 3.12.0rc0, < 3.17.0、3.17.0で修正済み