FlawOpen

flawopen.com/AI Security/mlflow-webhook-ssrf-cve-2026-64849

● CVE-2026-64849 · CVSS 8.6 · High
FlawOpen Security Research

CVE-2026-64849: MLflow Webhook SSRF

CVE-2026-64849 is an unauthenticated server-side request forgery in MLflow webhook delivery (High, CVSS 3.1: 8.6). The initial URL check did not constrain redirect targets or the address used by the actual connection; the webhook test endpoint could return the upstream response body.[1]

💡 Plain English Explainer (ELI5)

A receptionist checks the address written on a parcel, then lets the courier follow a forwarding note without checking the new destination. MLflow checked the webhook URL before connecting, but redirects and a second DNS lookup could send the request somewhere else. The fix checks the address of the actual connected peer before sending HTTP data.[1], [2]

Core Concepts & Subsystem Terms

SSRF
A server makes a network request chosen by a caller, crossing the server’s network trust boundary.[1]
Webhook test endpoint
MLflow API that sends a test event to a configured webhook; on affected default servers its response body was returned to the caller.[1]
DNS rebinding / TOCTOU
A hostname resolves to a public address during validation and a different, non-public address at connection time.[1], [2]

Framework mappings

Step-by-Step Attack Flow

Step 1

A reachable MLflow server accepts a webhook test

The advisory describes an unauthenticated default Tracking Server configuration. Authentication plugins and deployment controls can change reachability; confirm the actual configuration. CISA added the CVE to its Known Exploited Vulnerabilities catalog on August 19, 2026; inclusion signals known exploitation, but does not establish compromise of any particular deployment.[1], [4], [5]

Step 2

The first URL passes validation

_validate_webhook_url resolves and checks the original hostname, but the checked address is discarded rather than bound to the later socket.[1]

Step 3

A redirect or second lookup reaches another destination

The HTTP client follows redirects, and the connection can resolve the hostname again. The webhook test response reflects the upstream body, making this a full-read SSRF.[1]

Step 4

The upstream fix checks the connected peer

PR #24258 validates the socket peer immediately after connect and before TLS/HTTP data; its session also avoids environment proxy routing. MLflow 3.15.0 fixes the CVE; separate IPv6-transition bypasses in the guard were fixed in 3.16.0. The advisory credits freeman-bb as the original private reporter and AUTHENSOR as an independent finder.[1], [2], [3], [6]

Source Code: Flaw vs. Secure Implementation

These excerpts show the original URL-validation/request boundary and the upstream connection-time peer check. They omit exploit requests and internal targets. The 3.16.0 IPv6 normalization is a later, separate hardening change; do not merge those follow-up advisories into CVE-2026-64849.[2]

UNPATCHED FLAW
# Excerpt from mlflow/webhooks/delivery.py before PR #24258
_validate_webhook_url(webhook.url)  # checks the initial URL only
return session.post(
    webhook.url, data=payload_bytes, headers=headers, timeout=timeout
)  # redirects follow by default; the validated IP is not pinned
HARDENED SECURE PATCH
# Excerpt from the upstream connection-time fix
def _assert_public_peer(sock):
    if _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():
        return
    try:
        peer_ip = sock.getpeername()[0]
    except OSError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Could not determine webhook connection peer address: {exc}"
        ) from exc
    try:
        ip = ipaddress.ip_address(peer_ip)
    except ValueError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection resolved to an invalid IP address: {peer_ip!r}"
        ) from exc
    if not ip.is_global:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection blocked: {ip} is not a public IP address. "
            "This may indicate a DNS rebinding attempt."
        )


class _SSRFProtectedHTTPConnection(HTTPConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock


class _SSRFProtectedHTTPSConnection(HTTPSConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock

adapter = SSRFProtectedHTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.trust_env = False
session.mount("http://", adapter)
session.mount("https://", adapter)

Engineering & System Hardening Checklist

Sources

← AI Security Hub Directory →