CVE-2026-116692:MLflow 第三方 Scorer 反序列化漏洞可導致遠端程式碼執行
CVE-2026-116692: CVE-2026-116692: Remote Code Execution in MLflow via Third-Party Scorer Deserialization
MLflow 的 CVE-2026-116692 是第三方 GenAI 評分器反序列化漏洞,可讓攻擊者在 tracking server 遠端執行任意程式碼。
CVE-2026-116692: Remote Code Execution in MLflow via Third-Party Scorer Deserialization
Vulnerability ID: CVE-2026-116692
CVSS Score: 9.8
Published: 2026-10-27
CVE-2026-116692 (GHSA-26p8-2jq9-3vq9) is a remote code execution vulnerability in MLflow affecting versions 3.12.0rc0 prior to 3.17.0. The flaw stems from an overly permissive module prefix check during the deserialization of third-party GenAI evaluation scorers in mlflow/genai/scorers/base.py. By setting an experiment's artifact root to a local package directory within the Python module search path and uploading a crafted Python file as a run artifact, an attacker can invoke the /ajax-api/3.0/mlflow/scorer/invoke endpoint to dynamically import and execute arbitrary code on the MLflow tracking server.
TL;DR
An insecure module prefix check in MLflow's third-party GenAI scorer deserialization allows attackers to upload malicious Python artifacts into writable package directories and execute arbitrary code via dynamic module importation on unauthenticated or authenticated MLflow servers.
⚠️ Exploit Status: POC
Technical Details
- CVE / GHSA ID: CVE-2026-116692 / GHSA-26p8-2jq9-3vq9
- CWE Classification: CWE-94 (Code Injection) / CWE-470 (Unsafe Reflection)
- CVSS v3.1 Score: 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- Affected Versions: MLflow >= 3.12.0rc0, < 3.17.0
- Patched Version: MLflow 3.17.0
- Vulnerable Component: Scorer.model_validate() in mlflow/genai/scorers/base.py
- Exploit Status: Proof of Concept (PoC) Published
- CISA KEV Status: Not Listed
Affected Systems
- MLflow Tracking Server (PyPI package: mlflow)
- Official MLflow Docker Container Images (ghcr.io/mlflow/mlflow)
-
mlflow: >= 3.12.0rc0, < 3.17.0 (Fixed in:
3.17.0)
Code Analysis
Commit: a732443
Match third-party scorer modules exactly instead of by package prefix (GHSA-26p8-2jq9-3vq9) (#25906)
diff --git a/mlflow/genai/scorers/base.py b/mlflow/genai/scorers/base.py
index f281a919f45a8..11f50032d553f 100644
--- a/mlflow/genai/scorers/base.py
+++ b/mlflow/genai/scorers/base.py
@@ -576,10 +576,10 @@ def model_validate(cls, obj: Any) -> "Scorer":
module_path = data.get("module") or ""
class_name = data.get("class")
metric_name = data.get("metric_name")
- if not any(
- module_path == m or module_path.startswith(m + ".")
- for m in THIRD_PARTY_SCORER_ALLOWED_MODULES
- ):
+ # Exact match only: a dotted descendant of an allow-listed package can be a
+ # caller-placed file (e.g. a run artifact under a `file://` experiment root),
+ # and `import_module` would execute it before the class check below.
+ if module_path not in THIRD_PARTY_SCORER_ALLOWED_MODULES:
raise MlflowException.invalid_parameter_value(
f"Third-party scorer '{serialized.name}': module '{module_path}' is not "
f"in the allow-list {sorted(THIRD_PARTY_SCORER_ALLOWED_MODULES)}."
@@ -589,6 +589,21 @@ def model_validate(cls, obj: Any) -> "Scorer":
f"Third-party scorer '{serialized.name}': missing required fields in "
f"third_party_scorer_data (class, metric_name)."
)
+ # The wrappers resolve unknown metric names by splicing them into an import
+ # path (`ragas.metrics.collections.<metric_name>`, `deepeval.metrics.<metric_name>`),
+ # so a dotted name would reach a caller-placed module the same way.
+ if not metric_name.isidentifier():
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': metric_name '{metric_name}' "
+ "must be a plain identifier."
+ )
+ # Concrete subclasses pin `metric_name` via ClassVar and inherit the wrapper's
+ # `__init__`, so a `metric_name` kwarg would override the validated value above.
+ if "metric_name" in (data.get("kwargs") or {}):
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': kwargs must not contain "
+ "'metric_name'; set it at the top level of third_party_scorer_data."
+ )
try:
module = importlib.import_module(module_path)
except ImportError as e:
@@ -602,6 +617,11 @@ def model_validate(cls, obj: Any) -> "Scorer":
f"Third-party scorer '{serialized.name}': class '{class_name}' not "
f"found in module '{module_path}'."
)
+ if not (inspect.isclass(scorer_class) and issubclass(scorer_class, Scorer)):
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': '{module_path}.{class_name}' "
+ "is not a Scorer subclass."
+ )
init_kwargs: dict[str, Any] = dict(data.get("kwargs") or {})
# Two shapes of third-party class: (a) base wrappers (`RagasScorer` etc.)
# have no `metric_name` ClassVar — pass it as a kwarg; (b) concrete
diff --git a/mlflow/genai/scorers/scorer_utils.py b/mlflow/genai/scorers/scorer_utils.py
index 527166fd45769..d303fe44d4e02 100644
--- a/mlflow/genai/scorers/scorer_utils.py
+++ b/mlflow/genai/scorers/scorer_utils.py
@@ -41,12 +41,26 @@
"`mlflow.genai.evaluate(..., scorers=[...])` without calling `.register()`."
)
-# Restricts dynamic imports during third-party scorer deserialization to this
-# closed set so a malicious payload can't turn `model_validate` into arbitrary import.
+# Restricts dynamic imports during third-party scorer deserialization to this closed set
+# of the modules that define the supported scorer classes, so a malicious payload can't
+# turn `model_validate` into arbitrary import. Matched verbatim, never by package prefix:
+# a caller who can place a file below one of these packages (for example through a
+# `file://` experiment artifact root) must not be able to have the server import it as
+# `mlflow.genai.scorers.<pkg>.<run_id>.artifacts.<module>`.
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
+ "mlflow.genai.scorers.ragas.scorers",
+ "mlflow.genai.scorers.ragas.scorers.agentic_metrics",
+ "mlflow.genai.scorers.ragas.scorers.comparison_metrics",
+ "mlflow.genai.scorers.ragas.scorers.rag_metrics",
"mlflow.genai.scorers.deepeval",
+ "mlflow.genai.scorers.deepeval.scorers",
+ "mlflow.genai.scorers.deepeval.scorers.agentic_metrics",
+ "mlflow.genai.scorers.deepeval.scorers.conversational_metrics",
+ "mlflow.genai.scorers.deepeval.scorers.rag_metrics",
+ "mlflow.genai.scorers.deepeval.scorers.safety_metrics",
"mlflow.genai.scorers.trulens",
+ "mlflow.genai.scorers.trulens.scorers.agent_trace",
"mlflow.genai.scorers.phoenix",
})
Exploit Details
- GitHub Security Advisory: Official advisory documenting the 4-step reproduction chain and proof-of-concept execution against ghcr.io/mlflow/mlflow:v3.14.0.
Mitigation Strategies
- Upgrade MLflow to version 3.17.0 or later to enforce exact allowlist matching and class inheritance checks during scorer deserialization.
- Enforce read-only filesystem permissions on Python installation directories (site-packages and sys.path) for the OS user running the MLflow server process.
- Run MLflow server containers as a non-root user with a read-only root filesystem (--read-only).
- Configure remote object storage backends (S3, GCS, Azure Blob) for artifact storage rather than local file:// filesystem paths.
- Restrict network access to the MLflow tracking server and block untrusted access to /ajax-api/3.0/mlflow/scorer/invoke via a reverse proxy or WAF.
Remediation Steps:
- Identify all deployed instances of MLflow across virtual machines, Kubernetes clusters, and container registries running versions >= 3.12.0rc0 and < 3.17.0.
- Update the MLflow package to version 3.17.0 or higher using 'pip install --upgrade "mlflow>=3.17.0"' or by pulling the updated v3.17.0+ container image.
- Audit existing experiments in the MLflow tracking store to verify that no experiments have an artifact_location pointing to local system or Python site-packages directories.
- Reconfigure container and service definitions to run the MLflow server under a dedicated non-root service account with no write permissions to Python library paths.
- Restart the MLflow tracking server and verify that requests to /ajax-api/3.0/mlflow/scorer/invoke with dotted descendant module paths are rejected with an invalid parameter exception.
References
- GitHub Security Advisory: GHSA-26p8-2jq9-3vq9
- CVE-2026-116692 Official Record
- MLflow Pull Request #25906: Match third-party scorer modules exactly
- MLflow Fix Commit a732443e695c3c677680c54320efc394c81654ab
- MLflow v3.17.0 Release Notes
- MITRE ATT&CK Framework Mapping
- CISA Known Exploited Vulnerabilities Catalog
Read the full report for CVE-2026-116692 on our website for more details including interactive diagrams and full exploit analysis.
來源:DEV Community · dev.to