Semaphore UI 与 HFS:我们报告的两个授权漏洞
GHSA-ffp6-w57q-758q 与 GHSA-c3w7-ff56-j345:在 Semaphore UI 中,Manager 可以把其他项目的 Vault 移到自己的项目里;在 HFS 中,匿名访客可以替换另一位访客刚刚上传的文件。我们发现了什么,以及应安装哪些版本。

2026 年 11 月 9 日,不到一个小时之内,两个截然不同的开源项目发布了安全公告,致谢名单里是同一个名字。第一个是 Semaphore UI,许多团队用这个 Web 界面在自己的服务器上运行 Ansible 和 Terraform。第二个是 HFS,这个文件服务器从 2003 年起就让人们只用浏览器就能共享自己电脑上的文件夹。在 Semaphore 中,一个项目的用户可以夺走另一个项目中模板的 Vault 配置。在 HFS 中,在禁止删除的文件夹里,一位匿名访客可以替换另一位匿名访客刚刚上传的文件。
这就是安全公告 GHSA-ffp6-w57q-758q(在 Semaphore UI 2.19.20 中修复)和 GHSA-c3w7-ff56-j345(在 HFS 3.4.1 中修复)。它们由我们报告:两项发现都来自 Webcracy 联合创始人、道德黑客 Liam Giannini(GitHub 上的 Liam-G)。感谢 Semaphore 的维护者和 HFS 的作者 Massimo Melina,他们在同一天发布了安全公告和修复版本。
这两个缺陷看起来毫不相干,却有着相同的根源。在两种情况下,程序都检查了身份,只是检查错了对象。
Webcracy 与网络安全
除了开发网站和应用,Webcracy 也从事网络安全的各个方面:我们保护网络和服务器,用我们的 SOC 昼夜监控,发生事件时及时介入,提供审计、咨询和系统加固,并通过渗透测试和代码审查检验防御。研究我们自己和客户所使用的开源软件,是这项工作的一部分。Liam 用攻击者的方法寻找漏洞,但目的是让它们被修复:他私下向维护者报告,只有在修复发布后才公开。这与我们处理 ProjectSend、exarch 和 FreeScout 时遵循的 负责任披露 流程相同。
Semaphore UI:从一个项目跑到另一个项目的 Vault
为什么 Semaphore UI 很重要
Semaphore UI 是一个 Web 界面和 API,可以不经过命令行就运行 Ansible、Terraform、OpenTofu、Terragrunt、Pulumi 和 PowerShell。它从 2014 年就已存在,当时还叫 Ansible Semaphore,用 Go 编写,采用 MIT 许可证。它在 GitHub 上有超过 14,400 颗星、1,300 多个 fork 和 180 多位贡献者;官方 Docker 镜像 semaphoreui/semaphore 的下载量超过 860 万次,旧镜像 ansiblesemaphore/semaphore 还有约 60 万次。项目官网称安装量超过 10 万:这是厂商的数字,但数量级与其他数据一致。除了开源版本,还有付费的 Pro 版和 Enterprise 版。
在 Ansible 生态中,它是 AWX 和 Red Hat Ansible Automation Platform 的轻量替代品:只是一个二进制文件,可以用 SQLite 数据库启动,也不像 AWX 那样需要 Kubernetes 集群。因此,想要一个 Ansible 界面、又不想再搭建一套基础设施的人经常选择它。
而在这里,授权漏洞的分量比别处更重。这类工具保存着 SSH 密钥、云平台凭据,以及解锁加密机密的密码:它是发往整个基础设施的命令的起点。正因如此,它按项目划分,同一个安装也常常服务于多个团队或多个客户。项目之间的边界,实际上就是“谁能动什么”的边界。
什么是模板的 Vault
在 Semaphore 中,模板 描述一项要执行的任务:哪个 playbook、针对哪个清单、使用哪些变量。很多 playbook 包含用 Ansible Vault 加密的机密,解密需要密码。模板通过一个或多个 Vault 绑定 指定密码的来源:项目中保存的密钥,或者从外部系统获取密码的脚本。在数据库中,每个绑定是 project__template_vault 表中的一行,有自己的数字 ID,以及所属的项目和模板。
缺失的检查在哪里
创建模板要调用 POST /api/project/{project_id}/templates。中间件会正确检查用户是否是 URL 中项目的成员、是否可以管理其资源,Manager 角色具备这一权限。新模板被分配给该项目,但 vaults 列表是按客户端发送的原样进入数据库的。
缺陷出在 UpdateTemplateVaults 函数中。对于每个 ID 不为零的条目,它都假定这是一个需要更新的已有绑定,并执行:
UPDATE project__template_vault
SET project_id=?, template_id=?, vault_key_id=?, name=?, type=?, script=?
WHERE id=?
这一行只按 id 选取,不检查它属于哪个项目,而更新恰恰改写了 project_id 和 template_id,把它们交给攻击者的新模板。之后删除多余绑定的操作虽然按项目过滤,但执行时这一行早已被移走。
因此,攻击者只需在自己的项目中创建一个模板,并在 vaults 列表里填入受害项目某个绑定的 ID。把类型设为 script 会清空密钥字段,所以不需要知道对方项目的任何其他信息。而这些 ID 是数据库按顺序分配的数字,并不是秘密。检查针对的是 URL 中的项目,但被修改的对象却由请求正文中的一个数字决定:CWE 目录称之为 CWE-639(通过用户控制的键绕过授权)和 CWE-863(授权不正确)。
严重程度
请求之后,受害项目的模板失去了 Vault 绑定,而攻击者的模板拥有了它。最可能的结果是:需要解密机密的运行会失败,直到有人重新创建配置。我们在测试中没有测量这一点:它是合理的推断,而不是已证实的结果。
我们同样准确地说明 没有 发现的内容:没有读取机密,没有使用受害者的密钥,也没有执行代码。此外,攻击者需要在至少一个项目中拥有 Manager 角色或同等权限。因此严重程度为中等。但在一个编排基础设施的工具里,“中等”依然意味着:用户可以修改自己完全没有角色的项目,而负责其他团队或客户项目的人根本不会察觉。
我们如何验证
这个问题是在阅读 Semaphore UI 2.19.18(提交 b621d14)的代码、沿着 vaults 列表从路由一直追踪到 SQL 查询时发现的。为确认问题,Liam 用 Go 编写了一个回归测试,使用该版本的内存 SQLite 存储和原有组件:项目中间件、权限检查、模板创建和 SQL 层。测试创建 A 和 B 两个项目,只让用户成为 A 的 Manager,在 B 中创建一个带 Vault 的模板,然后请求在 A 中创建模板并带上 B 的绑定 ID。API 返回了 201,B 的模板的绑定从一个变成零个,而 A 的新模板包含了 B 原来的那个。
同样的查询至少从 2.11 版本起就存在于 Semaphore 的代码中。安全公告只列出了我们验证过的 2.19.18,并没有确定哪些更早的版本可以以同样方式被利用。稳妥的做法是假定它们可以。
2.19.20 中的修复
2.19.20 版本 包含了修复,即 pull request #4414 中的提交 9445ee9。更新不再触碰 project_id 和 template_id,并且只有当这一行确实属于已授权的项目和模板时才会找到它:
UPDATE project__template_vault
SET vault_key_id=?, name=?, type=?, script=?
WHERE id=? AND project_id=? AND template_id=?
来自其他项目的 ID 不再产生任何效果。维护者还添加了一个重现安全公告场景的测试 TestManagerCannotMoveForeignProjectVault。在报告中,我们还建议在更新没有匹配到任何行时拒绝请求,并在同一个事务中创建模板及其绑定:已发布的修复堵住了攻击路径,其他措施则由维护者决定。
如果你在使用 Semaphore UI
请至少升级到 2.19.20,更好的是升级到几分钟后发布的 2.19.21。根据其发布说明,它修复了另一个问题:用户无需知道当前密码,就能通过 API 修改自己的密码。使用 Docker 时,请固定一个明确的版本而不是 latest。
升级之后,请检查使用 Ansible Vault 的模板是否仍然保留着绑定:如果某个模板无缘无故丢失了 Vault,现在你知道了一个可能的原因。也请重新检查谁拥有 Manager 角色、在哪些项目中。如果两组用户之间的隔离确实至关重要,比如同一服务商的不同客户之间,那么单个安装中的项目是一道有用但并非绝对的边界:使用独立实例和独立数据库,可以限制下一个授权错误造成的损害。
HFS:刚上传的文件,另一位匿名访客就能替换
为什么 HFS 很重要
HFS(HTTP File Server)是那种在 IT 行业工作过几年的人几乎一定见过的程序。网名 rejetto 的 Massimo Melina 从 2000 年代初就开始开发它:在自己的电脑上启动,选择要共享的文件夹,任何知道地址的人都能用浏览器下载或上传文件,中间没有任何云服务。在 SourceForge 上,该项目自 2003 年以来的下载量超过 97 万次,还不包括从作者网站下载的次数。HFS 3 用 TypeScript 在 Node.js 上重写,采用 GPL-3.0 许可证,可在 Windows、Linux、macOS、FreeBSD 和 Android 上运行。它在 GitHub 上有近 4,000 颗星和 370 多个 fork;其发布包的下载量超过 50 万次,Docker 镜像 rejetto/hfs 超过 11.6 万次,npm 包在过去一年中约 4.4 万次。
它适合在局域网中传递文件,或接收客户和合作者发来的文件:在这些场景中,允许匿名上传但禁止删除的文件夹是再正常不过的配置。谁都可以放进文件,谁也不能拿走。
留给 WebDAV 的十秒钟
从 2026 年 4 月发布的 3.1.0 版本开始,HFS 支持 WebDAV,这个协议可以把共享文件夹作为网络驱动器挂载到 Windows 文件资源管理器或 Mac 的 Finder 中。有些 WebDAV 客户端的行为比较特别:例如 Finder 会先创建一个空文件,然后立即用真正的内容覆盖它。但覆盖文件需要删除权限,而“仅上传”的文件夹并不授予这一权限。
为了不让这些客户端出错,HFS 会给刚用已识别的 WebDAV 客户端上传文件的人十秒钟时间来替换它,即使没有删除权限。这是一个合理的折中。问题在于 HFS 如何判断谁是“刚上传文件的人”。
所有匿名访客都叫同一个名字
这个临时许可以一个由两部分组成的键保存:文件路径和用户名。对注册用户来说这没问题,甚至还有一个自动测试,检查两个不同账户不能使用对方的许可。但对于没有登录的人,HFS 返回的用户名是空字符串。所有匿名访客的名字都一样,因此键也一样。
于是,第二位匿名访客无需第一位的 Cookie 或凭据,就能在十秒内向同一路径发送 PUT 请求,只要 User-Agent 中包含 WebDAV 这个词。HFS 找到许可后,会在处理新上传之前就删除现有文件,同时绕过 can_delete 检查,以及把匿名上传绑定到上传者会话的保护机制。如果新上传成功,文件就被替换;如果失败或被中断,原文件也已经被删除了。默认开启的 force_webdav_login 选项也拦不住这个请求,因为不带 WebDAV 专用请求头的普通 PUT 不会被当作 WebDAV 认证请求处理。CWE 目录同样将其归为 CWE-863(授权不正确)。
严重程度
条件比较苛刻:需要一个允许匿名上传、禁止删除的文件夹,一个刚由 WebDAV 客户端上传的文件,知道它的路径,并在十秒内发出第二个请求。更早的文件无法触及。严重程度为中等。
但在这个时间窗口内,影响是实实在在的:一个刚刚发布、可能已经通过地址分享给别人的文件,任何人都可以在同一个地址下把它换成不同的内容。对于收件文件夹来说,这意味着你收到的文件可能并不是发件人发出的那一份。
我们如何验证
Liam 在 HFS 3.3.4(提交 bb6f718)的代码中追踪了处理流程,并确认同样的代码仍然存在于主分支中。随后,他构建了服务器,并在一个无法访问网络的 Docker 容器中运行,uploads 文件夹配置为允许上传、禁止删除。第一个匿名会话使用 Windows WebDAV 客户端的 User-Agent 上传了内容为 first 的文件。没有 Cookie 的第二个会话尝试删除它,HFS 正确地返回了 403。随后,同一个第二会话向同一路径发送了内容为 second 的 PUT:HFS 返回 200,原地址上的文件内容变成了 second。
两次请求来自同一个 IP 地址,但键中既不包含会话,也不包含 IP:从代码来看,来自不同地址的结果也会相同,不过我们没有单独测试这种情况。安全公告将 3.1.0 至 3.4.0 的所有版本列为受影响版本。
3.4.1 中的修复
11 月 9 日发布的 3.4.1 版本 在发布说明中只列出了一项改动,即提交 9a33df2:临时许可现在绑定到用户名,对于匿名访客则绑定到其会话 ID。两者都没有时,不授予许可。这正是我们提出的修复方案。新的自动测试检查了两个方面:另一个匿名会话会收到 403,而上传者本人仍然可以像 Finder 那样覆盖文件。
如果你在使用 HFS
请升级到 3.4.1 或更高版本。HFS 有自动更新功能,即使作为服务运行也可以使用,所以通常很快就能完成。如果由于某种原因无法立即升级,而你又有允许匿名上传的文件夹,请考虑至少在安装修复之前,要求用户登录后才能上传。
更一般地说,允许匿名上传的文件夹本身就是一扇敞开的门。它在很多场景下有意义,但更适合用来接收文件,而不是向他人发布文件,并且应定期检查其中的内容。
同一个错误,出现了两次
Semaphore 检查用户是否可以在 URL 中的项目里 工作,然后修改了一个由请求中的数字选定的对象。HFS 检查文件是否由 同一个用户 上传,但使用的名字对所有匿名访客都一样。两种情况下检查都存在,写得也很用心,其中一个甚至有测试覆盖。缺少的是那个正确的问题:我正在检查的身份,真的属于我即将操作的那个人和那个对象吗?
这类错误在正常使用应用时是看不出来的。只有把一个值从请求一路追踪到它被使用的地方,并且每次都问“这个值是谁决定的?”,才能发现它们。这是我们工作的一部分。
我们为什么研究开源软件
正是像 Semaphore 和 HFS 这样的工具,让一个小团队能够管理几十台服务器,或者不依赖云服务就能交换文件:开源、免费、易于安装。但应用越普及,其内部检查就越重要。阅读我们使用和推荐的项目的代码,并报告我们的发现,是我们回馈这些项目的方式,也让我们清楚自己在为客户安装什么。
如果你在使用自动化工具、文件服务器,或者有多个用户和角色的应用,想知道权限是否真的可靠,我们很乐意和你聊聊。我们的网络安全工作包括网络防护与监控、事件响应、审计、系统加固和渗透测试,而且总是从这里开始:阅读代码和配置,找出哪里缺少检查,并准确告诉你我们验证了什么、没有验证什么。