15 · 文件、数据与安全边界
ComfyUI 同时处理模型文件、用户上传、生成输出、工作流设置、执行历史和可选资产索引。它们并不都存在同一个数据库,也不共享同一种安全策略。
最重要的纠正: Prompt 队列和默认执行 history 是进程内存;
user/comfyui.db不会自动把未完成 Prompt 变成可恢复任务。
数据版图
folder_paths:逻辑目录注册表
核心目录不是到处硬编码,而是通过 getter/setter 与 folder_names_and_paths 注册:
- checkpoints、loras、vae、embeddings 等模型类别;
- input、output、temp、user;
- custom_nodes 与 web extensions;
- legacy 名称到当前类别的映射。
节点和路由通过逻辑类别查路径。extra model paths 可以为同一类别添加多个根,并携带支持扩展名集合。
annotated filepath
字符串可带尾部 annotation:
foo.png [input]
bar.webp [output]
preview.png [temp]annotated_filepath() 分离文件名与 base directory;无 annotation 时常以 input 为 fallback。
Annotation 是协议内的命名空间,不是允许客户端给任意绝对目录。
路径包含检查
安全目标:用户给出的相对路径解析后仍必须位于授权根目录。
is_within_directory():
- 对 base 与 target 都做
realpath(); - 用
commonpath()比较; - 对 null byte、跨 Windows drive 等 ValueError fail closed。
只检查字符串里有没有 .. 不够:根内 symlink 可以指向根外。只检查 startswith(base) 也不够:/data/output-evil 会匹配 /data/output 前缀。realpath + commonpath 才表达目录边界。
get_annotated_filepath() 越界时抛 ValueError;exists_annotated_filepath() 对越界返回 false,避免用存在性接口探测任意路径。
图片上传
POST /upload/image:
- 根据
type选 input/output/temp 根; - 规范化 subfolder;
- 绝对化目标并检查 commonpath;
- 创建子目录;
- 若不允许 overwrite,检测同名同内容;
- 同名异内容自动加
(1)、(2); - 写文件;
- 可选登记 Assets;
- 返回 name/subfolder/type。
hash compare 的作用
已有同名文件时,读取两者计算 hash;内容相同就不重复写入。它是上传体验优化,不是全局 content-addressed store:不同比例路径/文件名仍可存同内容,除非 Assets hash 层进一步去重。
Mask 上传
Mask route 还接收 original_ref,读取原图、保留 PNG metadata,并用上传图片 alpha 替换原 alpha。它再次检查 filename、subfolder 与授权目录,防止 original_ref 变成任意文件读写原语。
/view 文件读取
查看端点支持:
[input]/[output]/[temp]或 type 参数;- subfolder;
- webp/jpeg 缩略预览与 quality;
- RGB 或 alpha channel 转换;
- 可选
blake3:asset hash 解析; - 普通文件响应。
每条分支都必须保持相同路径边界。新增一种“按 hash 查看”不能绕过 owner / reference 授权;Assets resolver 返回经过服务层解析的绝对路径和下载名。
Stored XSS 防护
用户可以上传文件,服务又在 UI 同源下返回它。如果 HTML、JavaScript 或 SVG 被 inline 打开,脚本可能获得 ComfyUI origin 权限。
集中危险类型集合包括:
HTML / XHTML
JavaScript / ECMAScript
CSS
SVG
XML 与 +xml 家族
MHTML / RFC822危险内容默认:
- 改为/按 octet-stream 处理;
Content-Disposition: attachment;X-Content-Type-Options: nosniff;- 正确转义 filename;
- 对依赖
Sec-Fetch-Dest的响应设置适当Vary/ cache 语义。
SVG 作为 <img> 加载时浏览器使用受限 static mode,代码只在可信的 Sec-Fetch-Dest: image 情况下允许这一特例;缺 header 时 fail closed。
这段设计说明 MIME 安全不仅是“设置 Content-Type”,还涉及 disposition、nosniff、请求目的和缓存重放。
用户目录与多用户模式
UserManager 默认只有 default。启用 --multi-user 后,用户记录保存在 users.json,请求通过 comfy-user header 选择 profile;userdata 路径限制在该用户 public root。
它不是强身份认证
客户端 header 选择已存在 user ID,核心逻辑主要提供 profile 数据隔离和 UI 存储。不要把它等价于有密码、session、RBAC 的互联网多租户认证层。
需要公网服务时,应在受信反向代理/网关层实现认证、TLS、请求限制和 header 注入/清洗,并限制 ComfyUI 监听面。
System Users
内部系统用户使用保留前缀 __。HTTP 请求不能选择此前缀,创建普通用户也拒绝此前缀,防止外部 profile 假冒内部主体。
userdata
读取、列出、写入和删除 userdata 均先调用 get_request_user_filepath():
- URL decode 后重新绝对化;
- base root 和 user root 两级 commonpath 检查;
- 创建目录也只能在允许父路径内;
- glob 列表从已验证 root 开始。
任何“先验证编码前字符串,再 decode”都可能引入 traversal;这里在含 % 时先 unquote 再做最终绝对路径检查。
SQLite 与数据库启动
默认 URL:
sqlite:///<absolute ComfyUI>/user/comfyui.db可用 --database-url 覆盖,包括内存 SQLite。启动会取得 .lock 文件、设置 SQLite pragma、执行迁移,并在特定迁移路径创建备份。
文件锁用来阻止两个进程无协调地迁移/写同一 SQLite。横向多实例不应简单共享本地 DB 文件;应选择支持并发的外部数据库策略,并确认当前服务实际支持的 dialect/迁移。
Prompt history 为什么不是数据库历史
PromptQueue.task_done() 把:
prompt
outputs / UI metadata
execution status
extra_data(敏感项已分离)写入内存 history,最大约 10,000 项,之后淘汰旧记录。服务重启会消失。
数据库文件存在并不改变这一事实。若业务要求审计、重试或跨重启查询,应在外层消费事件/结果并持久化,或明确实现 durable queue,而不是依赖 /history。
Assets:可选的内容与引用层
只有 --enable-assets 才启动路由、数据库同步和 background scanning;--enable-asset-hashing 进一步计算 blake3,默认关闭以避免大模型扫描成本。
为什么拆 Asset 与 AssetReference
Asset 表达内容身份(hash、大小、MIME);AssetReference 表达某用户/路径/名字对内容的引用,以及 mtime、缺失状态、preview、metadata、job ID。
同一内容可有多个路径/用户引用;文件移动或缺失不必抹掉内容身份。hash 为 null 的 seed asset 还能先快速建索引,后续 enrichment 再计算。
Asset Seeder
启动时若启用 Assets,后台 seeder 扫描 models/input/output:
prune_first标记/处理消失路径;- 批量插入 seed assets 与 references;
- 可暂停、恢复、取消;
- 通过事件报告进度;
- Prompt 执行期间可 pause,任务结束 enqueue output enrichment 后 resume;
- hashing 开关控制重 I/O。
暂停扫描避免它与模型加载/输出写入争用大量磁盘和 CPU;完成后只针对 output roots 增量 enrich,比每次全盘重扫更合适。
输出资产登记
节点 UI 输出在 execution.py 里先经 enrich_output_with_assets(),再写 ui_outputs、cache 和 WebSocket。即使没有连接客户端,资产仍可登记;asset ID 也会进入 history/cache。
这体现一个重要边界:业务持久化不能依赖“消息是否成功发给某个 Socket”。发送路径是易失的,资产登记发生在结果处理路径。
安全边界清单
| 边界 | 核心已有防护 | 仍需部署方负责 |
|---|---|---|
| 浏览器 → 本地服务 | Host/Origin、可选 CORS、CSP | 不随意开放公网、网关认证 |
| Prompt → 节点 | schema/类型/范围/环验证 | Python 插件可信度与资源配额 |
| 文件名 → 磁盘 | realpath/commonpath、根目录类别 | 文件权限、磁盘 quota、恶意大文件 |
| 文件 → 浏览器 | MIME、attachment、nosniff、Sec-Fetch-Dest | 反向代理别覆盖关键 header |
| user header → profile | 已知 ID、系统前缀拒绝、目录隔离 | 真正身份认证与 header 防伪 |
| secret → API node | sensitive extra data 分离 | secret store、域名策略、日志审计 |
| model file → Python | safetensors / safe load 路径 | 模型来源、hash、依赖供应链 |
| custom node → 进程 | allow/deny/manager policy | OS/container 隔离与代码审计 |
威胁建模的几个具体场景
场景 1:../../etc/passwd
最终绝对路径不在 input/output root,commonpath 检查拒绝;存在性接口也返回 false,避免旁路探测。
场景 2:output 内 symlink 指向系统目录
realpath 展开 symlink 后 target 落在 root 外,被 is_within_directory 拒绝。只做 abspath 会漏掉这一类。
场景 3:上传恶意 SVG 再直接打开
MIME 被判危险,document navigation 获得 attachment + nosniff;仅真正 <img> destination 可走受限 inline 特例,且缓存按目的区分。
场景 4:外部请求冒充 __system
UserManager 在读取 header 时先拒绝保留前缀,并使用同类未知用户错误减少 probing。
场景 5:自定义节点读取任意文件
如果节点自己调用 open('/etc/...'),folder_paths 无法阻止,因为 Python 插件已拥有进程权限。防护必须在插件审计与 OS 沙箱层。
运维建议
- 默认只监听 loopback;远程访问放在认证反代后。
- 对上传、输出、模型盘分别设置权限、容量和备份策略。
- 生产环境使用 custom-node allowlist 和锁定依赖。
- 优先 safetensors,并记录模型内容 hash 与来源。
- 不把
/history当审计数据库;外部持久化 prompt ID、参数、结果与错误。 - 开启 Assets 前评估模型盘大小;hashing 可能带来明显启动/扫描 I/O。
- 保留关键安全响应头,反向代理不要把 attachment 改回 inline。
- 多租户需求使用进程/container 隔离,不只依赖
comfy-user。
可迁移的设计方法
- 内容对象与路径/用户引用分表,支持去重、移动、缺失和多 owner。
- 文件边界检查必须在 decode、join、realpath 之后做最终判定。
- 同源用户文件的安全响应同时考虑 MIME、disposition、nosniff 与 cache。
- 易失执行状态与应用数据库明确分层,不制造“好像持久化”的错觉。
- 持久化副作用放在结果处理路径,不依赖客户端连接是否存在。