exarch:我们报告的符号链接漏洞
GHSA-9r5q-fvpf-p55p:TAR 或 ZIP 压缩包可以利用目标位置已有的符号链接,把文件写到解压目录之外。Webcracy 的 Liam Giannini 发现了什么,以及为什么应将 exarch 升级到 0.8.0 或更高版本。

解压一个压缩包,看上去是世界上最无害的操作:选一个文件夹,打开 ZIP,文件就放到那里。但当压缩包来自外部,比如用户上传的、从外部服务下载的、邮件附件里的,文件最终落在 哪里 就成了安全问题。一个精心构造的压缩包可以尝试写到所选文件夹之外:写进配置文件,写进服务器启动时执行的脚本,写进另一个用户的目录。
exarch 正是为了防止这种情况而诞生的库。它的核心用 Rust 编写(exarch-core),同时也以 Python 包(exarch)和 Node.js 包(exarch-rs)的形式发布。README 把它定位为 Python 的 tarfile 和 Node.js 的 tar-fs 等库的安全替代品,而后者恰恰在这类漏洞上有着很长的历史。它的核心承诺只有一个:压缩包里的文件始终留在目标目录内。
从 0.6.1 版本起,直到 0.8.0(不含)之前,这个承诺是可以被绕过的。这就是安全公告 GHSA-9r5q-fvpf-p55p 所描述的缺陷,公告于 2026 年 11 月 2 日发布,并在 exarch 0.8.0 中修复。这个漏洞是我们报告的:发现者是 Liam Giannini(GitHub 上的 Liam-G),白帽黑客、Webcracy 联合创始人。感谢 exarch 的维护者,他确认了问题,并在几天内发布了修复。
Webcracy 与网络安全
Webcracy 做的不只是网站和应用。网络安全是我们工作的核心部分:渗透测试、代码审查、配置分析,以及对我们和客户每天使用的开源库和程序进行研究。Liam 的工作是白帽黑客:他用和攻击者相同的方法寻找漏洞,但目标正好相反。他发现的问题会私下报告给能够修复的人,并且只在修复发布后才公开。这个流程叫做 负责任的披露,我们这次也是这样做的,就像 10 月报告的 FreeScout 漏洞 一样。
解压时发生了什么
要理解这个问题,需要两个要素。
第一个是 符号链接:文件系统中的一种条目,表现得像一个文件夹,实际上却指向另一个位置。在默认配置下,exarch 会拒绝 压缩包中包含的 符号链接:这是防御恶意压缩包的经典手段之一。但它没有考虑另一种情况:在解压开始之前,目标目录中 就已经存在的符号链接。这种情况比想象中更常见:共享文件夹、在现有安装上直接解压的更新包、uploads 或 cache 指向另一块磁盘的环境。
第二个要素是 已创建目录的缓存。为了不对每个文件重复同样的检查,exarch 会记住解压过程中已经创建过的目录,并把它们视为可信。问题在于,用来创建目录的 Rust 函数 create_dir_all,在路径已经存在且是指向某个目录的符号链接时,同样会返回成功。于是,exarch 把一个实际上通往别处的链接记录成了“我自己创建的目录”。
攻击者只需要一个包含两个完全普通条目的压缩包:一个目录 redirect/ 和一个文件 redirect/pwned.txt。如果目标目录中已经存在名为 redirect 的链接,指向一个可写的外部目录,第一个条目就会让这个链接进入可信目录缓存。在验证第二个条目时,exarch 跳过了父路径的解析,因为它被视为可信;也跳过了完整路径的解析,因为压缩包里没有符号链接。打开文件时使用的 O_NOFOLLOW 标志只保护路径的最后一个组成部分,不保护中间的目录。结果是:解压顺利完成,文件出现在 outside/pwned.txt,位于所选目录之外,内容由准备压缩包的人决定。
在默认安全配置下,TAR 和 ZIP 都受到影响。而 7z 文件的解压不受影响,因为按照设计,它在路径验证中不使用目录缓存。
有多严重
实际影响取决于应用运行在什么环境中。最坏的情况是 在目标目录之外任意写入文件,只受进程权限的限制。如果启用了允许覆盖的 skip_duplicates = false 选项,通过该链接可以访问到的现有文件也可能被截断或替换。由此可能导致配置被篡改、攻击者获得持久化、服务中断,或者当写入的文件随后被其他组件读取或执行时,导致代码执行。
不过,这里有一个必要条件:符号链接必须已经存在于目标目录中,而且攻击者必须知道它的名字。因此严重程度为中等,属于 CWE-59 类别,即在访问文件前对链接解析不当。但中等并不等于无关紧要:这个缺陷击中的,恰恰是人们选择 exarch 这类库的唯一理由。
我们是如何验证的
问题是在阅读 exarch 0.6.1 源代码时发现的:我们跟踪了一个压缩包条目从目录缓存(DirCache)到 SafePath 中的验证,再到最终打开文件的整个路径。为了确认问题,Liam 编写了两个回归测试,一个针对 TAR,一个针对 ZIP,它们在内存中构造压缩包,并解压到专门创建的临时目录中,测试环境为 Linux 和 Rust 1.98.0。在存在漏洞的版本上,两个测试都失败了,因为解压成功,文件逃出了目标目录。
在提交报告的同时,我们还准备了一个最小修复:在 TAR 和 ZIP 的验证中,不再把“在缓存中”当作安全的证明,使其与 7z 代码已有的做法保持一致。应用这个修改后,两个回归测试都通过了,现有的 801 个单元测试和 136 个集成测试也全部通过。
0.8.0 中的修复
维护者做的比最小补丁更多。在 pull request #816 中(已包含在 0.8.0 版本 里),缓存被彻底移除,取而代之的是一项检查:对每个写入的文件,用 lstat 检查目标目录下路径的每一个组成部分。父路径每次都会被解析,除非它就是目标目录的根。该修复适用于 TAR、ZIP 和 7z,包括原子解压模式。
有一个行为变化需要了解:默认情况下,只要目标目录中存在任何符号链接,解压现在都会失败,即使这个链接指向目标目录内部。需要使用符号链接的人可以通过符号链接选项重新启用,此时只有留在目标目录内部的链接才会被接受。把符号链接本身用作目标目录仍然是允许的。
pull request 中坦率地说明了一个遗留限制:基于路径的检查在验证和写入之间留下了一个时间窗口,解压期间已经对目标目录拥有写权限的人可以利用它。11 月 5 日发布的 0.9.0 版本 在其他改动之外,把写入操作锚定到目录描述符上,这正是针对这种情况推荐的解决方案。这是一个很好的例子,说明一次漏洞报告可以成为加固整个子系统的契机,而不仅仅是堵上一个漏洞。
如果你在使用 exarch,该怎么做
如果你使用的 exarch-core、Python 包 exarch 或 npm 包 exarch-rs 版本低于 0.8.0,请升级。考虑到 0.9.0 在文件写入方面增加了更多保护,建议直接升级到最新可用版本,同时核对发布说明中列出的不兼容 API 变更。升级之后,还要检查你的解压操作是否会因为目标目录中合法的符号链接而失败:这是新的默认行为,最安全的做法是解压到一个空的专用目录中。
其实这条建议适用于任何解压库,而不仅是 exarch:把来自外部的压缩包解压到一个由你自己的进程新建的空目录里,并使用只拥有必要权限的用户来运行。这样,即使遇到这样的缺陷,也找不到现成的链接可以利用。
我们为什么研究开源软件
企业里运行的大量软件,包括那些自认为没用开源软件的企业,都是由 exarch 这样的开源库构成的,而这些库往往只由一个人或一个小团队维护。代码是公开的,但总得有人用想要攻破它的眼光去读它。花时间审查这些项目,并负责任地报告我们发现的问题,是我们做贡献的方式,也让我们确切地知道自己为客户部署到生产环境里的是什么。
如果你的应用接受用户上传的文件,处理来自外部的压缩包、文档或图片,或者只是从来没有被专门找漏洞的人看过,我们很乐意聊一聊。我们的 渗透测试 工作正是从这里开始:阅读代码和配置,弄清楚哪里的数据没有经过检查,并准确告诉你我们验证了什么、没有验证什么。