Pickle sur gRPC : RCE non authentifiée sur un serveur de politique robotique
LeRobot, de Hugging Face, faisait transiter son canal d'inférence robot-vers-politique via pickle sur gRPC non authentifié — tout hôte atteignant le port obtenait l'exécution de code à distance. Le correctif de juin 2026 supprime pickle.
De quoi s’agit-il ?
Le 29 juin 2026, l’équipe de recherche de SecureLayer7 a publié l’analyse d’une faille d’exécution de code à distance non authentifiée dans LeRobot, le framework de robotique open source de Hugging Face. Le pipeline d’inférence asynchrone de LeRobot scinde une politique robotique en un couple client–serveur : un client sur le robot physique diffuse ses observations vers un PolicyServer hébergé sur un hôte GPU distinct, qui renvoie les actions. Le transport est gRPC — et les charges utiles étaient véhiculées sous forme d’octets pickle bruts.
La faille découle logiquement de ce choix. Cette analyse s’appuie sur l’advisory public et le correctif amont ; aucun exploit fonctionnel n’est reproduit ici. La leçon relève de l’architecture de déploiement plutôt que du modèle : lorsqu’un composant de service désérialise des données réseau non fiables avec pickle, et que le canal n’a aucune authentification, le réseau est la vulnérabilité.
Comment ça marche
Le pickle de Python n’est pas un format de données — c’est un petit bytecode à machine à pile dont les opcodes invoquent des appelables arbitraires au chargement. CPython le dit clairement : « Le module pickle n’est pas sûr. Ne désérialisez que des données de confiance. » Tout objet peut définir ce qui se produit lors de sa désérialisation ; une charge forgée peut donc exécuter une commande dès qu’elle est désérialisée, avant toute validation en aval.
Dans la version affectée, les gestionnaires d’inférence asynchrone lisaient directement sur le fil des octets accessibles à l’attaquant et les passaient à pickle.loads() :
# motif vulnérable (illustratif)
policy_specs = pickle.loads(request.data) # nosec <-- s'exécute avant la validation
Deux problèmes se cumulaient. D’abord, la désérialisation avait lieu avant le contrôle de type censé valider le message : l’objet était donc déjà construit — et c’est à la construction que s’effectue le travail dangereux. Ensuite, l’écouteur gRPC était lié via add_insecure_port(...) : pas de TLS, pas de jeton, pas d’authentification mutuelle. La topologie par défaut suppose que le robot et le serveur de politique partagent un réseau de confiance (un VPC ou un maillage Tailscale), mais dès que le port (TCP/50051 par défaut) devient accessible à un acteur non fiable, cette hypothèse s’effondre. La faille est en outre bidirectionnelle : le serveur sérialisait aussi ses réponses d’action, si bien qu’un serveur de politique malveillant ou compromis pouvait retourner la même primitive contre chaque client robot connecté.
Un indice récurrent dans le code : un commentaire # nosec accolé à chaque appel pickle — une annotation qui fait taire l’avertissement d’analyse statique (la règle B301 de Bandit) sans traiter le risque d’exécution qu’il signale.
Pourquoi c’est important
Le code affecté était livré dans le framework servant à entraîner et déployer des politiques de robots réels ; la population exposée inclut donc les clusters GPU de recherche mutualisés, les topologies d’entraînement multi-machines, les notebooks de démonstration laissés actifs, et les déploiements cloud où l’opérateur ignorait que gRPC était à l’écoute. Un seul port atteignable donne l’exécution de code sous le processus serveur — rapporté comme root dans le conteneur de référence — ce qui, sur un hôte GPU, place à un pas les artefacts de modèles, les jeux de données et les identifiants cloud.
L’ironie est cinglante : Hugging Face a écrit safetensors précisément pour éliminer « le problème pickle » sur les fichiers de modèles, et sa documentation déconseille en gras de charger des fichiers pickle de sources non fiables. Le même anti-motif ressurgit dans un autre recoin de la pile — le transport d’inférence — c’est-à-dire exactement là où les bugs de désérialisation ne cessent de se cacher dans l’écosystème du service d’IA.
Défenses
Ne jamais appeler pickle.loads() sur des données contrôlées par le réseau. Le correctif durable est un format de fil typé et non exécutable. Le patch amont a remplacé pickle par safetensors pour les tenseurs et JSON pour les métadonnées, enveloppés dans une structure portant une étiquette de type que le désérialiseur valide contre le type attendu. Cela supprime l’interpréteur d’opcodes, impose un schéma et rejette un message routé vers le mauvais gestionnaire.
Authentifier le transport. gRPC n’implique pas l’authentification — le mTLS ou l’authentification par jeton sont optionnels. Liez les points d’accès de service avec des identifiants, pas via add_insecure_port, et exigez un TLS mutuel entre robot et serveur de politique.
Considérer le réseau comme hostile. Refus par défaut du trafic entrant vers les ports d’inférence, écoute sur loopback ou une interface privée, et jamais d’exposition d’un port de politique/inférence à un réseau qu’un tiers non fiable peut atteindre. Segmentez les hôtes GPU à l’écart des charges de travail générales.
Traquer le motif statiquement. Recherchez dans les services Python exposés au réseau les pickle.loads portant sur toute donnée remontant à une entrée de requête — et traitez un # nosec voisin comme une anomalie, non comme une résolution.
Moindre privilège et contrôle de sortie. Restreignez le rôle cloud du processus de service au strict minimum, et alertez si un processus serveur de politique engendre un enfant inattendu (un shell, un interpréteur) ou établit des connexions sortantes qu’il ne devrait jamais faire.
Statut
| Élément | Détail |
|---|---|
| Référence | CVE-2026-25874 (voir la fiche cve.org) |
| Divulgation | Advisory publié le 29 juin 2026 |
| Affecté | Inférence asynchrone de LeRobot (PolicyServer / client robot) en v0.4.3 et antérieures |
| Cause racine | pickle.loads() sur entrée gRPC non authentifiée, désérialisée avant la validation de type |
| Impact | Exécution de code à distance pré-authentification sur le serveur de politique ; bidirectionnelle (un serveur malveillant peut atteindre les clients) |
| Correctif | PR amont #3048 — pickle retiré, remplacé par un schéma typé safetensors + JSON ; mettez à jour et activez l’authentification du transport / l’isolation réseau |