CVE 状态: 已申请,待分配。此发现以 GHSA-4qh7-66xv-h329 发布。CVE 分配后,本仓库将重命名为
CVE-YYYY-NNNNN-zotlit-PoC,且此横幅将替换为 CVE 链接。
| 研究员 | Dostxodjayev Abdullox (@squeeze440) |
| 公告 | GHSA-4qh7-66xv-h329 |
| CVSS 3.1 | 5.5(中危) |
| 弱点 | CWE-73、CWE-200 |
摘要
AidenLx ZotLit(aidenlx/zotlit)1.1.12 中附件导入功能存在文件名或路径的外部控制漏洞,攻击者若控制共享/同步的 Zotero 库,可通过构造的 linked_file 附件路径,将受害者文件系统中的任意本地文件泄露到受害者的 Obsidian 库中。
产品
ZotLit — 用于 Zotero 集成的 Obsidian 插件(aidenlx/zotlit,插件 ID zotlit,清单版本 1.1.12)
测试版本
Git 提交 41e60aaa6178629b34f8a42d104e757594b72435(2026-07-31)
预估 CVSS v3.1
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5(中危)
非显而易见的指标:AV:L — 利用发生在受害者的本地 Obsidian/zotlit 进程处理攻击者提供的 Zotero 条目元数据时(通过共享/同步库传递),这与“本地应用处理恶意文件”类漏洞的惯例一致,尽管传递本身可以通过网络进行(共享群组库、通过电子邮件发送的导出文件)。UI:R — 受害者必须将恶意库导入/同步到 Zotero,并在引用该构造附件的笔记上运行 ZotLit 的笔记导入(或注释/引用嵌入)功能;这是插件核心、默认启用功能的常规使用(attachment.import 默认为 true),并非异常操作。I:N/A:N — 此发现仅为读取/复制原语;未声称存在目标端路径遍历(相关但未经验证的观察见“详情”)。
详情
ZotLit 直接从 Zotero 的 SQLite 数据库(或同步/共享库)读取附件元数据,包括自由文本列 itemAttachments.path,并在解析“链接”附件的读取位置时完全信任该数据:
packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath(),"linked-absolute" 分支:
case "linked-absolute":
return parsed.path;
对于 linkMode: 2(linked_file)且其 path 不携带 attachments: 基目录占位符的行,parsed.path 是原始数据库字符串,被原样返回为要读取的绝对文件系统路径——没有允许列表,也不限制在 Zotero 数据目录或任何用户批准的位置内。
packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename(),"linked-absolute" 分支:
case "linked-absolute":
return basename(path.path);
最终效果:攻击者若能将 linked_file(linkMode 2)Zotero 条目放入受害者的库中——例如共享的 Zotero 群组库、受害者导入的 .rdf/.json/Better BibTeX 导出文件,或攻击者拥有写权限的同步库——即可将该条目的附件路径设置为受害者磁盘上的任意文件(~/.ssh/id_rsa、浏览器凭据存储、其他库、.env 文件等)。当受害者以默认 attachment.import: true 设置运行 ZotLit 的笔记导入(或渲染/嵌入该注释)时,ZotLit 会静默地将该文件内容复制到受害者的 Obsidian 库中,使用可预测的名称(<attachmentKey>-<basename>)。由于库通常会被同步、提交到 git 或发布,这会将攻击者原本无法触及的数据移动到攻击者(或任何其他有权访问同步目标的人)可读取的位置。
相关但未经验证的观察(不属于本发现的 PoC): attachmentFilename() 的兄弟分支 "storage"(zt-template-attach.ts:103-104)返回原始路径后缀,而未像其他两个分支那样应用 basename() 调用。这是否可独立利用取决于 Zotero 自身的存储同步机制如何命名本地下载的文件,这超出了本仓库范围且未经验证——此处仅标记为值得为一致性而修复的加固缺口。
概念验证
动态验证:由于此沙箱中无法进行完整的离线 pnpm 工作区安装(corepack/pnpm 工具链离线损坏),确切的易受攻击函数(parseAttachmentPath、attachmentAbsPath、attachmentFilename、copyAttachments/copyAttachment/writeCopy/destMatches、isErrno、reflink)从已发布源码中逐字提取(上文已注明文件:行号,逻辑未变),并直接在 Node 中执行。唯一的替换是将 LogTape 日志调用替换为 console.warn——仅为表面改动,对控制流零影响。脚本:~/engagements/zotlit/evidence/poc-verify.mjs。
步骤:
~/engagements/zotlit/evidence/victim-disk/id_rsa 模拟受害者机密(虚拟内容,明确标记为模拟)。{ key: "EVILKEY1", path: "<path to victim's id_rsa>", linkMode: 2 }——正是 getAttachmentByKey 从 Zotero 数据库返回的形状。attachmentAbsPath()——将源解析为受害者的文件,逐字,无验证。attachmentFilename()——将 id_rsa 解析为看似安全的目标基名。note-parser.ts:427 的真实 vaultName 构造(${key}-${filename})。copyAttachments()——结果 { copied: 1, skipped: 0, missing: 0 }。fake-vault/attachments/EVILKEY1-id_rsa——内容与受害者原始文件逐字节匹配。在真实 xterm 中完整运行的截图(命令 + 输出):../evidence/poc-run.png
影响
攻击者若能够影响受害者的 Zotero 库内容(共享/群组库、导入的参考文献导出文件或同步库),即可将受害者操作系统用户可读取的任意本地文件——SSH 密钥、凭据存储、其他库内容、.env/配置机密——泄露到受害者的 Obsidian 库中,无提示或确认,在受害者使用 ZotLit 核心笔记导入功能且采用默认设置时立即发生。库通常会被同步(Obsidian Sync、git、云文件夹)或发布,因此这将本地文件读取原语转化为现实的远程暴露。
弱点
修复建议
建议将 linked_file(linkMode 2)附件源限制在受害者明确配置或批准的文件系统位置(例如 Zotero 自身配置的 baseAttachmentPath,或 Zotero 数据目录),并在自动复制路径超出任何预期根目录的链接附件之前提示用户。作为纵深防御,考虑将已应用于 attachmentFilename() 的 linked-absolute/linked-base 分支的仅 basename() 清理同样应用于 "storage" 分支,以确保没有任何代码路径返回未清理的文件名片段。
致谢
Dostxodjayev Abdullox(GitHub:squeeze440)
报告渠道
aidenlx/zotlit 已启用私密漏洞报告——请通过仓库上的 GitHub 安全公告(GHSA)草稿报告,而非公开 issue。
用于构建库内副本的文件名通过 basename() 从同一攻击者控制的路径派生,因此目标文件名受攻击者影响,但不含路径分隔符(此分支上遍历安全)。
apps/obsidian/src/services/note-import/note-parser.ts:403-430 — resolveEmbeddedImage() 在将 Zotero 文献笔记的嵌入图像注释转换为 Obsidian Markdown 时被调用(ZotLit“导入笔记”功能的常规默认操作)。它解析 sourcePath = attachmentAbsPath(attachment, ...) 并调用 deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })`——将任意攻击者选择的绝对路径排队复制到库中。
apps/obsidian/src/services/attachment-import/service.ts:125-155 — AttachmentImportBatch.resolveLink() 将 { source: sourcePath, dest: <vault attachment folder>/<key>-<filename> } 排队,仅受 attachment.import 设置控制,其默认为 true(apps/obsidian/src/services/settings/schema.ts:123)。
apps/obsidian/src/lib/copy-attachments.ts:43-60 — copyAttachment() 执行实际的读+写,无路径验证:stat(source) 然后 copyFile(source, dest)。