Consul Template 的 writeToFile 辅助函数直接打开操作者提供的目标路径,并跟随被链接的路径组件,从而使渲染输出能够逃逸预期目录并覆盖已存在的文件。
我在审查 HashiCorp Consul Template 时发现了这个问题,当时脑海中有一个直接的文件系统安全问题:
如果 writeToFile 收到的路径看似位于预期目录之内,它是否会验证文件系统实际将数据写入何处?
在本例中,答案是否定的。
writeToFile 模板辅助函数通过 os.Create() 或 os.OpenFile() 直接打开用户提供的最终路径。
这些操作会跟随目标路径中已存在的符号链接、目录联接(directory junction)以及等效的文件系统重定向。
这意味着路径字符串可以始终保持在操作者预期的根目录之下,而实际写入却落在其他位置。
在我可控的概念验证中,一个被链接的父目录将渲染输出重定向到预期目录树之外,并导致一个已存在的目标文件被覆盖。
该问题最终成为 CVE-2026-14361。
HashiCorp 公告: HCSEC-2026-20
IBM 公告: CVE-2026-14361 安全公告
CVE: CVE-2026-14361
修复版本: 0.42.1
photo0
操作者提供的目标路径看似位于预期根目录内 -> 攻击者预先放置被链接的父组件或最终路径组件 -> writeToFile 直接打开该路径 -> 文件系统将写入解析到预期目录之外 -> 渲染的机密被重定向 -> 已存在的目标可能被覆盖
Consul Template 从 Consul 和 Vault 等来源渲染数据。
writeToFile 辅助函数允许模板将选定内容写入单独的本地文件,同时应用所请求的所有者、组和权限模式。
HashiCorp 的文档专门以 PKI 材料演示了该辅助函数:
private key -> writeToFile /my/path/to/cert.key
certificate authority -> writeToFile /my/path/to/cert.pem
certificate -> append to /my/path/to/cert.pem
这使得它不仅仅是一个普通的输出辅助函数。
跨越此边界的内容可能包括:
关键问题不在于 writeToFile 能否创建所请求的文件名。
真正的问题是:
该进程是写入操作者预期的文件系统位置,还是仅仅写入路径在打开时所解析到的任何对象?
在受影响版本中,它信任的是后者。
写入辅助函数是高价值的安全攻击面,因为它们从应用程序数据跨越到文件系统变更。
有趣的失败往往不是经典的 ../ 路径遍历。
而是解析失败:
当进程以比攻击者更高的文件系统权限运行时,这一点尤其重要。
低权限的本地攻击者可能无法直接覆盖敏感文件。
但如果他们能够影响预期写入目录下的某个路径组件,权限更高的 Consul Template 进程可能会替他们执行写入。
这就是我关注的安全边界。
我并没有将其当作一次泛泛的路径遍历审查。
所提供的路径并不需要 .. 段。
它可以始终在字面上保持在预期根目录之内。
更强的问题是:
在写入敏感内容之前,是否会拒绝被链接的目标组件?
这个问题对以下两种情况都至关重要:
第二种情况尤其值得关注,因为日志和配置中仍会显示预期目录下一个看似无害的路径。
而文件系统将其解析到其他位置。
根本原因是直接基于路径创建文件,而没有进行感知链接的验证。
在测试的修订版本中,writeToFile() 选择两种打开路径之一。
追加模式使用:
f, err = os.OpenFile(
path,
os.O_APPEND|os.O_WRONLY|os.O_CREATE,
perm,
)
普通写入模式使用:
dirPath := filepath.Dir(path)
if _, err := os.Stat(dirPath); err != nil {
err := os.MkdirAll(dirPath, os.ModePerm)
if err != nil {
return "", err
}
}
f, err = os.Create(path)
两种打开路径都没有在文件打开前拒绝被链接的目标组件。
这一点很重要,因为:
os.Create(path) 会跟随已存在的文件系统重定向并截断解析后的文件os.OpenFile(path, ...) 在追加模式下会跟随被链接的路径组件写入之后仍延续了同样的基于路径的假设。
所有者和权限再次通过路径来应用:
err = os.Chown(path, uid, gid)
err = os.Chmod(path, perm)
这意味着元数据操作也绑定到了一个可变的路径名上,而非已经打开的文件描述符。
因为攻击者可以在 writeToFile 运行之前预先布置好重定向。
基本攻击不需要概率性的竞态条件。
攻击序列非常简单:
writeToFile 直接打开它这就是整个漏洞。
诚然,操作系统在基于路径的文件打开过程中通常会跟随符号链接。
但这并不意味着这是安全的应用程序行为。
安全问题不是:
“Go 是否按文档所述运行?”
真正的问题是:
一个写入敏感渲染数据的辅助函数,是否验证了解析后的目标与操作者预期的位置一致?
在受影响版本中,它没有进行验证。
这一区别很重要,因为 Consul Template 可能:
在这种情境下跟随攻击者预先布置的文件系统重定向,会引发真实的权限与信任边界问题。
我围绕测试提交中 writeToFile 的确切行为构建了一个独立的复现程序。
可控环境使用了:
writeToFile 的可控机密内容复现流程如下:
观察到的行为是:
writeToFile 跟随了重定向这证实了该论断的两个部分:
最终组件的符号链接已经足以证明不安全的链接跟随。
但被链接的父组件证明了更强的操作性要点:
已存在的目标也很关键。
没有它,PoC 只能展示意外的文件创建。
通过从现有文件开始并验证其最终哈希,复现程序直接证明了覆盖行为。
这使得结果比仅基于源代码的论断更加具体。
该问题需要本地文件系统的影响能力。
攻击者需要足够的访问权限,才能在预期写入位置或其下方创建或修改符号链接、目录联接或等效重定向。
随后,Consul Template 进程必须通过该路径进行写入。
影响程度在很大程度上取决于进程权限和渲染内容。
实际可能造成的结果包括:
这不是默认安装下可利用的远程未认证任意写入。
但在高权限或共享文件系统部署中,这是一个明确的本地信任边界失败,并具有切实的影响。
HashiCorp 将该问题归类为:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
公布的基准评分为:
4.7 / Medium
该向量反映了:
官方公告还明确指出,重定向的写入可以覆盖已存在的文件。
评分适中是因为利用取决于本地路径控制条件,而非因为文件系统影响是理论性的。
HashiCorp 公告列出:
Affected: consul-template up to and including 0.42.0
Fixed: consul-template 0.42.1
修复版本随 0.42.1 版本于 2026 年 7 月 8 日发布。
0.42.1 补丁在多个层面加固了写入路径。
修复后的辅助函数使用 os.Lstat() 检查:
并在这些组件是链接时予以拒绝。
这封堵了报告和回归测试所涵盖的直接父目录重定向与最终文件符号链接场景。
在受支持的 Unix 平台上,目标使用 O_NOFOLLOW 打开。
这样,如果在预检查与打开之间最终组件变成符号链接,打开操作本身就会失败。
这一点很重要,因为仅靠预检查就可能产生另一个 TOCTOU 窗口。
平台特定实现是 Windows 上的空操作(no-op),因为 Windows 无法通过相同机制使用 O_NOFOLLOW。
补丁将基于路径的所有者和模式操作替换为基于描述符的操作:
f.Chown(uid, gid)
f.Chmod(perm)
这将元数据更改绑定到实际打开的文件上,而不是稍后再次解析路径。
辅助函数现在仅在 stat 失败确实是 os.IsNotExist 时才创建父目录。
其他错误(例如权限失败)会被直接返回,而不会继续进入写入路径。
补丁添加了针对以下内容的专项测试:
公开的补丁讨论明确记录了一个重要的限制。
新的验证检查:
它不会遍历并拒绝每一个更上层的祖先组件。
维护者选择这一边界是因为 writeToFile 没有配置沙箱根目录来锚定完整的包含性检查,而且常见操作系统可能在路径前缀中包含合法的受管链接,例如 macOS 上的 /var -> /private/var。
O_NOFOLLOW 同样只保护最终组件,而非每一个祖先目录。
这并不会改变所报告漏洞的状态或官方修复版本。
但它确实明确了补丁所提供的精确安全属性:
这一区别值得在技术文章中保留。
我于 2026 年 3 月 21 日以第二个独立 Consul Template 发现的名义,私下向 HashiCorp 安全团队报告了此问题。
报告内容包括:
最初的跟进邮件并未出现在安全团队的报告队列中,很可能是被邮件列表垃圾邮件过滤器拦截了。
在我重新转发完整报告后,HashiCorp 联系了工程团队,并将其与第一个 Consul Template 问题分开调查。
HashiCorp 在 0.42.1 中修复了该漏洞,并于 2026 年 7 月 8 日发布了 HCSEC-2026-20。
IBM 为同一 CVE 发布了相应的安全公告。
两份官方公告都将该报告归功于:
Mohamed Abdelaal (0xmrma)
核心教训很简单:
路径字符串并不等同于它所命名的文件系统对象
只要特权代码写入受攻击者影响的路径,这一区别就至关重要。
仅仅检查字符串是否以预期目录开头是不够的。
即使是没有遍历序列的干净路径,也可能通过以下方式解析到其他位置:
敏感操作必须绑定到身份已在正确边界得到验证的目标。
该问题还强化了一条更广泛的规则:
当你已经拥有打开的文件描述符时,应通过该描述符执行安全敏感操作,而不是再次解析路径
这正是基于描述符的 Chown 和 Chmod 更改至关重要的原因。
writeToFile 被文档化为用于证书和私钥等敏感材料os.Create 或 os.OpenFile 打开目标Chown 和 Chmod 引入了额外的可变路径信任问题0.42.1 版本添加了链接检查、受支持平台上的 O_NOFOLLOW、基于描述符的元数据操作以及回归测试这个漏洞与 ../ 遍历无关。
路径字符串看起来是正确的。
文件系统目标却不是。
Consul Template 接受了操作者预期的路径,跟随了攻击者预先布置的重定向,并将渲染内容写入了另一个文件。
这就是它成为 CVE-2026-14361 的原因。
已在 consul-template 0.42.1 中修复。