CritiqueCVE-2026-116692
MLflow vulnerable to remote code execution on unauthenticated servers via third-party scorer deserialization importing caller-placed artifact modules
Exploitable facilement et à distance, avec des conséquences graves : à corriger sans attendre.
- Sévérité
- Critique9.8 / 10
- Publiée le
- 2026 d’octòbre 27
- Mise à jour
- 2026 d’octòbre 27
L'avis
Traduction automatique de l'avis officiel. En cas de doute, l'avis d'origine, plus bas, fait foi.
Résumé
MLflow 3.12.0 a introduit la prise en charge de l'enregistrement et de la désérialisation de scoreurs tiers dans MLflow OSS. Lors de la désérialisation d'un scoreur, le serveur accepte un chemin de module Python contrôlé par la requête sous l'un des quatre préfixes de package autorisés, importe ce module, obtient une classe contrôlée par la requête et l'instante de manière synchrone.
Un appelant HTTP peut combiner ce comportement avec les endpoints intégrés de MLflow pour l'exécution, l'expérience et l'upload d'artefacts :
- Créer une expérience dont l'emplacement d'artefact local pointe vers un package de scoreur autorisé et inscriptible.
- Créer une exécution, ce qui fait allouer à MLflow
<artifact_location>/<run_id>/artifacts/. - Uploader un module Python dans le répertoire d'artefacts de cette exécution.
- Soumettre un scoreur sérialisé tiers dont le chemin de module résout vers le fichier uploadé.
- Déclencher l'importation et la construction de classe côté serveur via
/ajax-api/3.0/mlflow/scorer/invoke.
Sur le serveur sans authentification par défaut en utilisant l'image officielle MLflow v3.14.0, cela produit une exécution de code Python arbitraire non authentifiée avec les privilèges du processus du serveur MLflow. L'image reproduite exécute le processus en uid 0 et permet l'écriture dans son arbre de package Python installé.
La preuve de concept jointe est intentionnellement inoffensif. Le constructeur uploadé effectue une seule action : il écrit un marqueur aléatoire à côté de son propre artefact d'exécution uploadé. Le marqueur est ensuite télécharger via l'endpoint de downloading des artefacts d'exécution et vérifié pour une correspondance exacte.
Détails techniques
1. La liste blanche accepte les descendants créés par l'attacker
Au commit de publication 86cd7f5a1dc25a1387ad07c87811bbc30f62951c, mlflow/genai/scorers/scorer_utils.py définit ces racines autorisées :
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
Dans mlflow/genai/scorers/base.py, le désérialiseur accepte soit une racine exacte, soit un descendant :
module_path == m or module_path.startswith(m + ".")
Il effectue ensuite les opérations suivantes en utilisant les valeurs contrôlées par la requête :
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None)
scorer_instance = scorer_class(**init_kwargs)
Un préfixe de nom de package n'est pas une barrière de provenance. Si le processus du serveur peut écrire sous un package autorisé, un descendant créé par l'attacker peut satisfaire la vérification de préfixe chaîne et devenir un code Python importable.
Emplacements de source pertinents au commit de publication v3.14.0 :
mlflow/genai/scorers/scorer_utils.py, modules de scoreurs tiers autorisésmlflow/genai/scorers/base.py, désérialisation et construction de scoreurs tiers
2. La désérialisation se produit avant le traitement des traces
Dans mlflow/server/handlers.py, _invoke_scorer_handler() analyse le scoreur sérialisé fourni et appelle :
scorer = Scorer.model_validate_json(serialized_scorer)
Seulement après la désérialisation et la construction, l'handler appelle :
batches = get_trace_batches_for_scorer(trace_ids, scorer, tracking_store)
Le constructeur contrôlé par l'attacker s'exécute donc avant que le serveur ne valide ou ne résolve les IDs de trace fournis.
La classe de preuve minimale est délibérément pas un scoreur MLflow complet. Après que son constructeur a écrit le marqueur, la logique de batch de trace ultérieure accède aux attributs de scoreur que la classe de preuve n'implémente pas. L'HTTP 500 résultant se produit après l'exécution du code et ne invalide pas la preuve.
Aucune exécution de tâche en arrière-plan ou option de serveur d'exécution de tâche spéciale n'est nécessaire pour atteindre le constructeur.
3. La disposition des artefacts de MLflow place le module sous le préfixe approuvé
L'endpoint de création d'expérience accepte un artifact_location. Pour les expériences stockées par fichier, MLflow déduit chaque répertoire d'artefact d'exécution comme :
<artifact_location>/<run_id>/artifacts/
La preuve de concept sélectionne ce répertoire de package dans l'image officielle v3.14.0 :
file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens
Après avoir créé une exécution et uploadé audit_<token>.py, l'emplacement de système de fichiers résultat est :
/usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens/<run_id>/artifacts/audit_<token>.py
Le chemin d'importation correspondant est :
mlflow.genai.scorers.trulens.<run_id>.artifacts.audit_<token>
L'attaque ne remplace pas un module de package existant. Elle crée un nouveau module portée par l'exécution dans la disposition d'artefact stockée par fichier normale. L'ID d'exécution est retourné à l'appelant.
4. Comportement d'authentification et d'autorisation
Avec l'authentification désactivée, comme dans la configuration de serveur reproduite par défaut, aucun identifiant n'est requis.
Lorsque l'authentification de base MLflow est activée, l'authentification s'applique toujours. Cependant, l'endpoint AJAX d'invocation de scoreur est affecté à validate_gateway_proxy, qui retourne Vrai sans effectuer de vérification de permission de ressource au niveau de l'expérience ou du scoreur.
L'exploitation pratique dans un déploiement authentifié nécessite toujours que l'appelant crée une expérience et une exécution et leur upload des artefacts. Le problème d'autorisation clé est que la désérialisation de scoreur elle-même n'est pas contrôlée par une vérification de permission spécifique pour l'expérience fournie avant l'importation.
Preuve de concept
La reproduction jointe est intentionnellement isolée :
- image officielle exacte MLflow v3.14.0 pinée par digest de manifeste ;
- réseau Docker désactivé ;
- MLflow lié au loopback du conteneur ;
- aucune commande shell ou rappel externe ;
- aucune requête externe ;
- aucun écrit sur le système de fichiers hôte ;
- marqueur créé à côté de l'artefact d'exécution uploadé ;
- marqueur récupéré via l'endpoint de download des artefacts d'exécution ; et
- conteneur jetable supprimé après completion.
Exécuter :
./run-official-image.sh
Sortie attendue:
La suite de l'avis n'est pas traduite : elle figure dans l'avis d'origine, plus bas.
Composants touchés
| Composant | Versions touchées | Corrigé en |
|---|---|---|
| mlflowPython (pip) | >= 3.12.0rc0, < 3.17.0 | 3.17.0 |
Vous utilisez une version touchée ? Passez à la version corrigée, puis redéployez.