LightLLM CVE-2026-26220 : du pickle sur un WebSocket que le serveur force sur le réseau
CVE-2026-26220 (divulguée le 15 février 2026) place pickle.loads() sur deux endpoints WebSocket non authentifiés du mode prefill-decode de LightLLM — et le serveur refuse de se lier à localhost, donc la surface est toujours distante.
De quoi s’agit-il ?
Le 15 février 2026, le chercheur Valentin Lobstein (Chocapikk) a divulgué publiquement une exécution de code à distance non authentifiée dans LightLLM, un moteur d’inférence LLM en Python (~3 900 étoiles GitHub). Suivie sous CVE-2026-26220 et attribuée par VulnCheck le 16 février, la faille est notée CVSS 4.0 9.3 (critique) et classée CWE-502 — Désérialisation de données non fiables. Elle touche LightLLM ≤ 1.1.0.
C’est un cas d’école, avec une variante moderne. Le mode prefill-decode (PD) de LightLLM expose deux endpoints WebSocket qui appellent pickle.loads() sur des trames binaires brutes sans aucune authentification — et le serveur refuse explicitement de se lier à localhost, si bien qu’en mode PD la surface d’attaque est, par conception, toujours joignable sur le réseau. Elle appartient à la même classe récurrente que la CVE-2025-32444 de vLLM (CVSS jusqu’à 10.0), où du pickle non fiable transitant sur un canal inter-nœuds donnait un résultat identique.
Comment ça marche
Le module pickle de Python est un sérialiseur qui, par conception, peut reconstruire des objets arbitraires — y compris des objets dont la reconstruction exécute du code. Passer des octets contrôlés par un attaquant à pickle.loads() revient à confier au processus un script à exécuter. Ce n’est pas une subtilité propre à LightLLM : c’est une propriété documentée de pickle, et la raison d’être de CWE-502.
Le mode PD de LightLLM répartit l’inférence entre plusieurs nœuds : un PD master orchestre l’enregistrement des workers et les transferts de KV-cache entre des nœuds GPU prefill et decode distincts. Les workers se connectent au master en WebSocket pour s’enregistrer et remonter leur état. D’après la divulgation et l’issue de suivi du CVE, la désérialisation se trouve dans lightllm/server/api_http.py :
# /pd_register (api_http.py ~ligne 310)
data = await websocket.receive_bytes()
obj = pickle.loads(data) # untrusted network bytes -> object graph
# /kv_move_status (api_http.py ~ligne 331) — même schéma
upkv_status = pickle.loads(data)
Deux autres appels à pickle.loads() figurent dans la boucle PD côté worker. Y accéder ne demande aucune authentification : /pd_register réclame d’abord une trame JSON d’enregistrement, mais le node_id est un entier non validé et mode n’est qu’une vérification de chaîne — rien d’une authentification. /kv_move_status accepte du pickle dès la première trame.
Le détail aggravant est la contrainte de déploiement inscrite au démarrage :
assert manager.args.host not in ["127.0.0.1", "localhost"]
Le master doit se lier à une interface routable, puisque les workers distants doivent le joindre. Il n’existe pas de configuration « sûre » en loopback seul pour le mode PD. Une preuve de concept fonctionnelle figure dans le writeup du chercheur ; le mécanisme est le gadget reduce classique de pickle — un objet forgé dont la désérialisation invoque une commande système [REDACTED] — et nous ne reproduisons pas de charge exécutable ici. Le point structurel suffit : tout hôte capable d’ouvrir une socket vers le PD master obtient l’exécution de code avec les privilèges du processus de service, avant toute inférence du modèle.
Pourquoi c’est important
Ce n’est pas une attaque inédite — c’est une instance à fort impact d’une classe que l’écosystème de service ML ré-embarque sans cesse. La même cause racine (pickle sur un canal interne supposé de confiance) a produit la CVE-2025-32444 de vLLM. Le motif se répète parce que le service désagrégé multiplie les canaux inter-nœuds, et chacun qui parle pickle est une RCE latente.
Le rayon d’impact, c’est la couche d’inférence elle-même : des nœuds GPU qui détiennent généralement les poids du modèle, des clés d’API, des identifiants cloud, et une position privilégiée dans le réseau. Un attaquant qui exécute du code sur un PD master est déjà au-delà du périmètre qui compte. Et puisque les endpoints du PD master sont, par conception, exposés au réseau, une instance joignable depuis un segment non fiable (réseau interne à plat, security group trop large, port de cluster exposé) est exploitable sans le moindre identifiant.
Il y a aussi une leçon de processus. La divulgation note que le projet avait reçu des signalements de désérialisation antérieurs — #784 (ZMQ recv_pyobj, mars 2025) et un rapport privé dans #1102 (novembre 2025) — restés sans suite, d’où l’escalade en CVE plutôt qu’un traitement discret. Considérez par défaut les infrastructures ML à évolution rapide comme immatures côté sécurité : la vélocité du code est élevée, la cadence de revue de sécurité souvent non.
Défenses
Vous n’avez pas besoin d’un correctif éditeur pour neutraliser ceci. Les mesures sont de l’hygiène d’infrastructure standard et s’appliquent à toute situation de pickle sur le réseau, pas seulement à LightLLM :
-
N’exposez pas les endpoints PD à des réseaux non fiables. Puisque le master ne peut pas se lier à localhost, confinez-le par la couche en dessous : liez-le à une interface privée dédiée, restreignez le port aux IP de workers connues via pare-feu / security group, et placez le trafic PD sur un VLAN ou un mesh isolé. Ne laissez jamais
/pd_registerou/kv_move_statusjoignables depuis un sous-réseau généraliste. -
Authentifiez les canaux inter-nœuds. L’enregistrement des workers et le KV-status sont de l’IPC inter-nœuds, pas une API publique. Mettez devant un mTLS (certificats clients) ou un contrôle par secret/jeton partagé, pour qu’un pair non enregistré ne puisse même pas ouvrir la socket.
-
Remplacez pickle pour l’IPC réseau. Les données échangées ici sont un état structuré simple (entiers, chaînes, dictionnaires). JSON, MessagePack ou protobuf les transportent sans accorder d’exécution de code. C’est le correctif durable demandé aux mainteneurs dans #1213.
-
Si pickle est inévitable, contraignez-le. Utilisez un
RestrictedUnpickleravec une liste blanche explicite de classes sûres et rejetez tout le reste, et/ou enveloppez les trames de signatures HMAC pour que le serveur ne désérialise que des messages qu’il peut vérifier. -
Détectez le déploiement, puis l’abus. Inventoriez tout LightLLM lancé avec
--run_mode pd_master | prefill | decode. Alertez sur les connexions entrantes vers les ports PD hors de la liste blanche de workers, et sur les processus d’inférence qui engendrent des shells (sh -c,bash, enfants de typeos.system) — la trace runtime à fort signal d’un gadget de désérialisation qui se déclenche. -
Généralisez l’audit. C’est une classe à l’échelle du parc, pas un CVE isolé. Cherchez dans votre stack de service les
pickle.loads,recv_pyobj,torch.load(...)sur entrée non fiable, et les chargements joblib sur toute socket, file ou frontière HTTP. Chacun est un candidat CWE-502.
État
| Élément | Référence | Date | Notes |
|---|---|---|---|
| Divulgation publique + PoC | Chocapikk (V. Lobstein) | 2026-02-15 | pickle.loads() WebSocket en mode PD |
| CVE attribué | Advisory VulnCheck | 2026-02-16 | CVSS 4.0 9.3, CWE-502, affecte ≤ 1.1.0 |
| Suivi + correctif proposé | Issue GitHub #1213 | 2026-02 | Correctif demandé aux mainteneurs ; le projet a un historique de réponse lente en sécurité |
| Même classe, projet voisin | CVE-2025-32444 (vLLM) | 2025 | Pickle sur IPC inter-nœuds, CVSS jusqu’à 10.0 |
Le cadrage honnête : CVE-2026-26220 n’a rien de nouveau — c’est le monde du service ML qui réapprend que pickle sur une socket réseau non authentifiée, c’est de l’exécution de code à distance. Tant que les transports inter-nœuds n’abandonnent pas pickle, la défense vous revient : isolez le plan PD, authentifiez les pairs, et partez du principe que tout sérialiseur capable de reconstruire des objets arbitraires finira, un jour, par reconstruire ceux d’un attaquant.