ktransformers : RCE non authentifiée via pickle sur ZeroMQ (CVE-2026-26210)
Une RCE critique dans le moteur d'inférence ktransformers expose un socket ZMQ sur toutes les interfaces et déserialise via pickle tout ce qu'il reçoit. C'est le dernier cas du motif « ShadowMQ » recopié à travers les stacks d'inférence IA.
De quoi s’agit-il ?
CVE-2026-26210 est une faille critique d’exécution de code à distance (RCE) non authentifiée dans ktransformers, un moteur d’inférence LLM haute performance maintenu par KVCache.AI (environ 16 500 étoiles GitHub, connu pour faire tourner DeepSeek-V3 sur un seul nœud). Elle a été divulguée par Valentin Lobstein (Chocapikk) dans le cadre d’un audit de la déserialisation pickle dans les frameworks d’inférence ML, attribuée par VulnCheck, et publiée le 23 avril 2026. Le NVD la note CVSS 3.1 9.8 (et CVSS 4.0 9.3), la classe CWE-502 (déserialisation de données non fiables), et elle touche ktransformers jusqu’à 0.5.3.
La cause racine n’est pas inédite — et c’est tout l’intérêt. Il s’agit de la même association dangereuse de ZeroMQ et du module pickle de Python que celle documentée par Oligo Security en novembre 2025 sous le nom de ShadowMQ, un motif qui s’est propagé dans tout l’écosystème IA (Meta Llama Stack, vLLM, NVIDIA TensorRT-LLM, Modular Max, SGLang) en grande partie par recopie de code de serving.
Comment ça marche
Le backend balance_serve de ktransformers (son mode multi-concurrence historique, activé par --backend_type balance_serve) démarre un ordonnanceur qui attache un socket ZeroMQ ROUTER à toutes les interfaces réseau, puis relaie les messages entrants vers des threads de travail. Chaque worker déserialise les octets bruts qu’il reçoit :
# worker de l'ordonnanceur, simplifié
self.frontend.bind(f"tcp://*:{sched_port}") # écoute sur toutes les interfaces
# ... proxy ROUTER/DEALER vers les threads de travail ...
message = worker.recv()
data = pickle.loads(message) # CWE-502 : entrée non fiable
pickle.loads() n’est pas un parseur sûr : déserialiser un objet contrôlé par l’attaquant peut déclencher l’exécution de code arbitraire pendant l’unpickling. Comme le socket n’exige aucune authentification ni validation, tout hôte capable d’atteindre le port peut faire exécuter du code avec les privilèges du processus ktransformers. Nous omettons délibérément tout payload fonctionnel ; le mécanisme (un pickle malveillant se réduisant à une commande système) relève du CWE-502 classique et n’est décrit que pour expliquer le risque.
Deux détails rendent l’exploitation réaliste en production. D’abord, le déploiement Docker officiel tourne avec --network=host, ce qui supprime l’isolation réseau du conteneur et expose le port ZMQ sur l’hôte. Ensuite, bien que le port soit attribué dynamiquement, il est inscrit dans les logs du serveur et les sockets ZMQ se reconnaissent trivialement sur le réseau — Oligo a relevé des milliers de sockets ZMQ exposés sur l’Internet public. La fiche SSVC de la CISA signale une maturité d’exploitation « proof-of-concept » et un caractère automatisable.
Pourquoi c’est important
Les serveurs d’inférence se trouvent au cœur de l’infrastructure IA : ils détiennent les poids des modèles, les prompts, les clés d’API, et tournent souvent sur des clusters GPU avec un large accès au réseau interne. Une RCE sur un nœud peut signifier l’exécution de code à travers le cluster, un mouvement latéral, l’exfiltration de secrets et de modèles, ou du cryptominage GPU — la même classe d’impact que les cas ShadowMQ antérieurs.
La leçon plus large porte sur la chaîne d’approvisionnement par imitation. La faille ne s’est pas propagée via une dépendance partagée mais via des idiomes de code partagés — des motifs recv_pyobj() et « pickle sur ZMQ » recopiés entre projets en mouvement rapide, parfois avec le commentaire d’en-tête indiquant encore « Adapted from vLLM ». ktransformers n’est que la stack la plus récente à hériter du motif, ce qui veut dire que la suivante est probablement déjà en production.
Défenses
Le correctif est bien compris et le patch amont (PR #1944) le suit :
- Mettre à jour au-delà de 0.5.3, ou éviter complètement le backend
balance_serve. Le chemin de déploiement moderne intégré à SGLang ne démarre pas le socket ZMQ vulnérable. - Ne jamais faire
pickle.loads()sur des données non fiables. Utilisez un format non exécutable (JSON ou MessagePack) pour le RPC ; les requêtes d’inférence sont des données structurées qui n’ont pas besoin de pickle. C’est exactement ainsi que Meta, vLLM et Modular ont corrigé leurs variantes de ShadowMQ. - Attacher le socket à
127.0.0.1par défaut, pas àtcp://*. Exiger un opt-in explicite avant qu’un socket n’écoute sur une interface routable. - Authentifier le canal. ZeroMQ prend en charge CurveZMQ (et des schémas HMAC) ; même un secret partagé bloque l’exploitation opportuniste.
- Ne pas exécuter les conteneurs d’inférence avec
--network=host. Conservez l’isolation réseau du conteneur et n’exposez que l’API HTTP prévue. - Chasser l’exposition : scannez vos propres plages à la recherche des octets de salutation ZMQ et confirmez qu’aucun port d’ordonnanceur/RPC n’est joignable depuis l’extérieur du périmètre de confiance.
Statut
| Élément | Détail |
|---|---|
| CVE | CVE-2026-26210 (CWE-502) |
| Affecté | ktransformers ≤ 0.5.3 (backend balance_serve) |
| Sévérité | CVSS 3.1 9.8 / CVSS 4.0 9.3 (Critique) |
| Divulgation | Audit de code le 11 février 2026 ; CVE publiée le 23 avril 2026 |
| Correctif | PR amont #1944 ; préférer le déploiement intégré à SGLang |
| Crédit | Valentin Lobstein (Chocapikk), attribué par VulnCheck |
| Motif | « ShadowMQ » — réutilisation ZMQ + pickle à travers les stacks de serving IA (Oligo, nov. 2025) |
Sources
- → https://chocapikk.com/posts/2026/ktransformers-pickle-rce/
- → https://vulnerability.circl.lu/vuln/cve-2026-26210
- → https://www.vulncheck.com/advisories/ktransformers-unsafe-deserialization-rce-via-balance-serve
- → https://github.com/kvcache-ai/ktransformers/pull/1944
- → https://www.oligo.security/blog/shadowmq-how-code-reuse-spread-critical-vulnerabilities-across-the-ai-ecosystem