Consul Template 在模板评估期间验证了符号链接的指向位置,但其后的依赖项获取读取的却是原始路径。在这两个操作之间重新定向链接,会将沙箱内的文件引用变成沙箱外的文件泄露。
我在审查 HashiCorp Consul Template 时发现了这个问题,当时我带着一个非常具体的安全疑问:
如果 sandbox_path 在模板评估期间验证了符号链接,那么后续的文件读取是否仍然绑定到同一个已验证的目标?
在这个案例中,答案是否定的。
file 模板助手在模板评估期间解析了路径并强制执行了配置的沙箱。但在那次检查之后,它使用原始未处理的路径创建了一个文件依赖项。
该依赖项稍后才被获取。
如果攻击者在验证与依赖项获取之间的间隙中重新定向了符号链接,Consul Template 就可能读取沙箱外的文件。如果链接在下一次渲染前被恢复,沙箱验证会再次通过,缓存的沙箱外内容仍会被渲染。
该问题最终成为 CVE-2026-5061。
HashiCorp 公告: HCSEC-2026-12
IBM 公告: CVE-2026-5061 安全公告
CVE: CVE-2026-5061
修复版本: 0.42.0
photo0
attacker-controlled in-sandbox symlink -> sandbox validation resolves to safe target -> dependency stores original raw path -> attacker retargets link outside sandbox -> dependency fetch reads external file -> attacker restores safe link -> next validation passes -> cached external content is rendered
Consul Template 是一款用于渲染 Consul 和 Vault 数据的模板渲染工具。
它可以持续运行、监视依赖项的变更,并将数据渲染到文件或环境变量中供应用程序使用。
file 模板助手用于读取本地文件并将其内容插入到渲染输出中。
由于本地文件读取可能暴露进程可读取的机密信息,Consul Template 提供了 sandbox_path 作为该助手的边界。
文档中描述的安全属性非常明确:
file 的路径必须位于配置的沙箱之内这使得 sandbox_path 成为真正的安全边界。
关键问题不在于路径看起来是否位于沙箱之下。
真正的问题是:
被读取的文件是否仍然是那个通过了沙箱验证的文件?
在受影响版本中,答案是否定的。
文件系统沙箱的失败点往往在于路径验证与实际文件访问之间的间隙。
常见的模式是:
当攻击者可以在两个操作之间修改符号链接或等效的文件系统重定向时,这种假设就是不安全的。
Consul Template 使这个攻击面格外有趣,因为模板评估和依赖项获取是两个独立的阶段。
这种分离引出了正确的安全问题:
依赖项获取是绑定到已验证的目标,还是会再次解析攻击者可控的路径?
这就是我聚焦的边界。
根本原因是检查时间与使用时间(time-of-check to time-of-use)之间的不匹配,具体发生在:
template/funcs.go 中的沙箱验证dependency/file.go 中后续的依赖项读取在测试的修订版本中,fileFunc() 执行了以下操作:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
验证助手在检查包含关系之前正确地解析了符号链接:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
因此沙箱检查本身理解了解析后的目标。
但那个解析后的目标被丢弃了。
NewFileQuery() 转而存储了原始字符串:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
随后,依赖项获取再次读取了该路径:
data, err := os.ReadFile(d.path)
这就是整个漏洞。
代码检查了一次文件系统解析,随后却使用了另一次解析的结果。
因为符号链接是可变的。
攻击者不需要让沙箱外的目标直接通过 pathInSandbox()。
他们只需要在验证之后、Fetch() 执行读取之前,改变那个已获准的原始路径所解析到的目标。
具体时序是:
pathInSandbox() 期间,链接解析到一个安全文件FileQuery 记录了原始链接路径os.ReadFile(d.path) 再次解析该链接最后一步至关重要。
恢复安全链接并不会将已经获取的机密从依赖项缓存中移除。
重要的区别在于对明确安全控制的绕过。
这不仅仅是:
"Consul Template 监视期间文件发生了变化"
监视文件变化是预期行为。
真正的问题是:
一个通过了文档所述沙箱限制的路径,后来可能被用来读取该沙箱之外的文件
这是直接的可信边界失效。
应用程序已经做出了安全决策:
但后续的获取并没有与支撑该决策的目标绑定。
正是这一点把普通的文件系统可变性变成了漏洞。
我围绕确切的评估与依赖项获取时序构建了一个独立的复现程序。
可控的设置包括:
复现流程如下:
file 助手,使沙箱验证成功,并将原始路径注册为依赖项。os.ReadFile(d.path) 通过重定向后的链接进行读取。观察到的行为是:
这个哈希比较至关重要。
它证明了最终渲染的值不是过期的安全内容、文件名痕迹或错误路径的副作用。
它就是沙箱外文件的逐字节内容。
对于 TOCTOU 问题,最有力的证明必须掌控时间线。
仅仅展示符号链接可以指向沙箱外部是较弱的证据,因为 pathInSandbox() 在观察到外部目标时已经会拒绝它。
该安全论断依赖于按顺序证明所有这些状态:
这就是为什么复现程序明确分离了模板验证、依赖项获取、链接恢复和下一次渲染。
PoC 不依赖随机时序或反复猜测。
它以确定性的方式驱动了整个易受攻击的生命周期。
这使得根本原因和影响都更容易论证。
此问题需要本地文件系统影响力。
攻击者需要足够的权限,在易受攻击的时间窗口内创建、替换或重定向相关符号链接或等效的链接路径。
Consul Template 进程还必须:
这些条件很重要。
这并不是默认部署下未经身份验证的远程任意文件读取。
但在受影响的本地信任模型内,其影响是实实在在的:
sandbox_path 限制当进程拥有对敏感凭据、服务配置、令牌或私钥的访问权限时,风险会进一步增加。
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 0.41.4
Fixed: consul-template 0.42.0
修复于 2026 年 4 月 15 日 发布的 0.42.0 版本中。
修复方案很小,并且直接针对了被破坏的绑定关系。
补丁不再验证解析后的目标却用原始输入构建依赖项,而是返回解析后的路径,并将该路径传给 NewFileQuery():
resolvedPath, err := resolveSandboxedPath(
sandboxPath,
strings.TrimSpace(s),
)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(resolvedPath)
安全属性从:
变为:
这封堵了所报告的符号链接重定向路径,因为改变原始链接不再会改变依赖项存储的路径。
补丁还添加了一个针对性的回归测试,覆盖了确切的时序:
这正是修复此类 bug 时你想要的补救方式:
我于 2026 年 3 月 20 日 私下向 HashiCorp 安全团队报告了此问题。
报告内容包括:
HashiCorp 在 0.42.0 中修复了该问题,并于 2026 年 5 月 12 日 发布了 HCSEC-2026-12。
IBM 针对同一个 CVE 发布了相应的安全公告。
两份官方公告都将该报告归属于:
Mohamed Abdelaal (0xmrma)
核心教训很简单:
如果后续的文件操作可以将路径解析到不同的对象,那么仅验证路径是不够的
这条规则远远不止适用于 Consul Template。
任何执行以下操作的代码都适用:
更深层的安全属性不是:
"路径字符串曾经看起来是安全的"
而是:
敏感操作所使用的对象,必须是那个通过了验证的对象
在这个案例中,Consul Template 验证了一个解析结果,却获取了另一个。
这个间隙已经足够了。
sandbox_path 是一个明确的本地文件安全边界pathInSandbox() 正确地解析并验证了符号链接目标FileQuery 保留了原始的、受攻击者影响的路径os.ReadFile 再次解析了该路径0.42.0 版本通过将依赖项绑定到解析且验证后的路径修复了该 bug这个漏洞与绕过字符串前缀检查无关。
它关乎时间与身份。
Consul Template 在模板评估期间检查了链接指向哪里。 依赖项获取稍后又向文件系统问了同样的问题。 攻击者可以在这两个时刻之间改变答案。
这就是它成为 CVE-2026-5061 的原因。
已在 consul-template 0.42.0 中修复。