FlawOpen

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

● CVE-2026-64849 · CVSS 8.6 · Alta
Investigación · FlawOpen

CVE-2026-64849: SSRF en webhooks de MLflow

CVE-2026-64849 es una falsificación de solicitudes del lado del servidor (SSRF) sin autenticación en la entrega de webhooks de MLflow (alta, CVSS 3.1: 8.6). La comprobación de la URL inicial no limitaba los destinos de redirección ni la dirección usada por la conexión real; el endpoint de prueba podía devolver el cuerpo de la respuesta upstream.[1]

💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina una recepción que comprueba la dirección de un paquete, pero deja que el repartidor siga una nota de reenvío sin verificar el nuevo destino. MLflow comprobaba la URL del webhook antes de conectarse, pero las redirecciones y una segunda consulta DNS podían enviar la solicitud a otro lugar. La corrección comprueba la dirección del peer conectado antes de enviar datos HTTP.[1], [2]

Conceptos Clave y Términos

SSRF
El servidor realiza una solicitud de red elegida por quien llama y cruza el límite de confianza de su red.[1]
Endpoint de prueba de webhook
API de MLflow que envía un evento de prueba al webhook configurado; en servidores predeterminados afectados, devolvía el cuerpo upstream.[1]
DNS rebinding / TOCTOU
El hostname resuelve a una dirección pública durante la validación y a otra no pública al conectarse.[1], [2]

Mapeos de marcos

Flujo de Ataque Paso a Paso

Paso 1

Un servidor MLflow accesible acepta una prueba de webhook

El aviso describe un Tracking Server predeterminado sin autenticación. Los plugins de autenticación y los controles de despliegue pueden cambiar el acceso; comprueba la configuración real. CISA añadió el CVE a su catálogo Known Exploited Vulnerabilities el 19 de agosto de 2026; la inclusión indica explotación conocida, pero no demuestra que una implementación concreta haya sido comprometida.[1], [4], [5]

Paso 2

La URL inicial supera la validación

_validate_webhook_url resuelve y comprueba el hostname original, pero descarta la dirección comprobada en vez de asociarla al socket posterior.[1]

Paso 3

Una redirección o nueva resolución alcanza otro destino

El cliente HTTP sigue redirecciones y la conexión puede resolver de nuevo el hostname. La respuesta de prueba reflejaba el cuerpo upstream, lo que permitía SSRF de lectura completa.[1]

Paso 4

La corrección upstream verifica el peer conectado

PR #24258 valida el peer del socket justo después de conectar y antes de datos TLS/HTTP; la sesión también evita proxies del entorno. MLflow 3.15.0 corrige esta CVE; fallos IPv6 distintos se corrigieron en 3.16.0. El aviso atribuye el reporte privado original a freeman-bb y reconoce a AUTHENSOR como investigador independiente.[1], [2], [3], [6]

Código Fuente: Vulnerable vs. Seguro

Estos fragmentos muestran la frontera entre validar la URL y efectuar la petición, junto con la comprobación upstream del peer al conectar. Omiten solicitudes de explotación y destinos internos. La normalización IPv6 de 3.16.0 es una mejora posterior independiente.[2]

IMPLEMENTACIÓN VULNERABLE
# Fragmento de mlflow/webhooks/delivery.py anterior al 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
PARCHE SEGURO Y ROBUSTO
# Fragmento de la corrección upstream que comprueba el peer al conectar.
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)

Lista de Verificación de Seguridad para Ingeniería

Fuentes

← AI Security Directory →