FlawOpen

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

● CVE-2026-64849 · CVSS 8.6 · Haute
Recherche · FlawOpen

CVE-2026-64849 : SSRF des webhooks MLflow

CVE-2026-64849 est une falsification de requête côté serveur (SSRF) sans authentification dans la livraison des webhooks MLflow (élevée, CVSS 3.1 : 8.6). La validation de l’URL initiale ne contrôlait ni les cibles de redirection ni l’adresse de la connexion réelle ; l’endpoint de test pouvait renvoyer le corps de la réponse distante.[1]

💡 Explication en Langage Simple (ELI5)

Imaginez un accueil qui vérifie l’adresse d’un colis, puis laisse le livreur suivre une consigne de réexpédition sans contrôler la nouvelle destination. MLflow vérifiait l’URL du webhook avant la connexion, mais une redirection ou une seconde résolution DNS pouvait envoyer la requête ailleurs. Le correctif vérifie l’adresse du pair réellement connecté avant tout échange HTTP.[1], [2]

Concepts Clés et Termes

SSRF
Le serveur effectue une requête réseau choisie par l’appelant et franchit sa frontière de confiance réseau.[1]
Endpoint de test webhook
API MLflow envoyant un événement de test au webhook configuré ; sur les serveurs par défaut concernés, le corps distant était renvoyé à l’appelant.[1]
DNS rebinding / TOCTOU
Le nom résout vers une IP publique pendant la validation puis vers une autre adresse non publique à la connexion.[1], [2]

Correspondances de référentiels

Déroulement de l'Attaque Étape par Étape

Étape 1

Un serveur MLflow joignable accepte un test webhook

L’avis décrit un Tracking Server par défaut sans authentification. Les plugins d’authentification et contrôles de déploiement changent l’exposition ; vérifiez la configuration réelle. La CISA a ajouté cette CVE à son catalogue Known Exploited Vulnerabilities le 19 août 2026 ; cette inscription signale une exploitation connue, sans établir la compromission d’un déploiement particulier.[1], [4], [5]

Étape 2

L’URL initiale passe la validation

_validate_webhook_url résout et vérifie le nom initial, mais jette l’adresse vérifiée au lieu de la lier au socket ultérieur.[1]

Étape 3

Une redirection ou nouvelle résolution atteint une autre cible

Le client HTTP suit les redirections et la connexion peut résoudre à nouveau le nom. La réponse de test reflétait le corps distant, ce qui permettait une SSRF en lecture complète.[1]

Étape 4

Le correctif upstream vérifie le pair connecté

La PR #24258 valide le pair du socket juste après la connexion, avant TLS/HTTP ; la session évite aussi les proxys d’environnement. MLflow 3.15.0 corrige la CVE ; des contournements IPv6 distincts sont corrigés en 3.16.0. L’avis crédite freeman-bb comme premier signalement privé et AUTHENSOR comme découvreur indépendant.[1], [2], [3], [6]

Code Source : Vulnérable vs Sécurisé

Ces extraits montrent la frontière entre validation d’URL et requête, puis le contrôle upstream du pair à la connexion. Ils omettent les requêtes d’exploitation et les cibles internes. La normalisation IPv6 en 3.16.0 est un durcissement ultérieur distinct.[2]

IMPLÉMENTATION VULNÉRABLE
# Extrait de mlflow/webhooks/delivery.py avant la 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
PATCH SÉCURISÉ ET ROBUSTE
# Extrait du correctif upstream qui vérifie le pair au moment de la connexion.
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)

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources

← AI Security Directory →