0xmrma/CVE-2026-5061
GitHub: 0xmrma/CVE-2026-5061
CVE-2026-5061 概念验证与根因分析,演示通过符号链接 TOCTOU 竞争绕过 Consul Template 沙箱路径限制导致沙箱外文件泄露。
Stars: 0 | Forks: 0
# CVE-2026-5061
Consul Template 会验证符号链接在模板求值期间的指向,但其后的依赖项获取读取的却是原始路径。在这两个操作之间重新定向链接,会将沙箱内的文件引用转变为沙箱外的文件泄露。
## 简介
我在审查 **HashiCorp Consul Template** 时发现了这个问题,当时脑海中带着一个非常具体的安全疑问:
**如果 `sandbox_path` 在模板求值期间验证了一个符号链接,那么随后的文件读取操作是否仍然绑定在同一个已验证的目标上?**
在本例中,答案是否定的。
`file` 模板辅助函数会解析路径,并在模板求值期间强制执行配置的沙箱限制。但在该检查之后,它使用原始的原始路径创建了文件依赖项。
该依赖项随后被获取。
如果攻击者在验证和依赖项获取之间的空隙期重新定向符号链接,Consul Template 就可能读取到沙箱之外的文件。如果该链接在下一次渲染前被恢复,沙箱验证会再次通过,而缓存的外部内容仍会被渲染。
这个问题最终成为了 **CVE-2026-5061**。
**HashiCorp 安全公告:** [HCSEC-2026-12](https://discuss.hashicorp.com/t/hcsec-2026-12-consul-template-vulnerable-to-sandbox-path-bypass-in-file-helper-through-symlink-attack/77414)
**IBM 安全公告:** [CVE-2026-5061 安全公告](https://www.ibm.com/support/pages/security-bulletin-consul-template-vulnerable-sandbox-path-bypass-file-helper-symlink-attack)
**CVE:** [CVE-2026-5061](https://www.cve.org/CVERecord?id=CVE-2026-5061)
**修复版本:** `0.42.0`
## 攻击链
`受攻击者控制的沙箱内符号链接 -> 沙箱验证解析为安全目标 -> 依赖项存储原始路径 -> 攻击者将链接重定向至沙箱外 -> 依赖项获取读取外部文件 -> 攻击者恢复安全链接 -> 下一次验证通过 -> 渲染缓存的外部内容`
## Consul Template 的作用
**Consul Template** 是一款用于渲染 Consul 和 Vault 数据的模板工具。
它可以持续运行,监视依赖项的变更,并将数据渲染到文件或环境变量中供应用程序使用。
`file` 模板辅助函数用于读取本地文件,并将其内容插入到渲染输出中。
由于读取本地文件可能会暴露进程可读的机密信息,Consul Template 提供了 `sandbox_path` 作为该辅助函数的边界。
文档中记载的安全属性十分直接:
- 传递给 `file` 的路径必须位于配置的沙箱内
- 相对路径不得逃逸出沙箱
- 链接目标不得将允许的路径转变为对外部文件的读取
这使得 `sandbox_path` 成为了一个真正的安全边界。
重要的问题不在于该路径看起来是否位于沙箱之下。
真正的问题是:
**最终被读取的文件,是否仍然是那个通过了沙箱验证的文件?**
在易受攻击的版本中,并非如此。
## 为什么这个攻击面值得研究
文件系统沙箱经常在路径验证和文件访问之间的空隙中失效。
常见的模式是:
- 验证路径
- 将控制权交还给文件系统
- 稍后再次使用该路径
- 假定它仍然指向同一个对象
当攻击者能够在两次操作之间修改符号链接或等效的文件系统重定向时,这种假设就是不安全的。
Consul Template 使得这个攻击面尤为有趣,因为模板求值和依赖项获取是分开的阶段。
这种分离引出了那个关键的安全疑问:
这就是我关注的边界。
## 根本原因
根本原因在于以下两者之间存在检查时间到使用时间(TOCTOU)的不匹配:
- `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)` 再次解析该链接
- 读取了外部文件
- 获取到的值被缓存到模板大脑中
- 该链接在下一次模板求值之前被恢复
- 验证再次成功
- 缓存的外部内容被返回并渲染
最后一步至关重要。
恢复安全链接并不会从依赖项缓存中删除已经获取的机密信息。
## 为什么这是一个安全问题,而不仅仅是文件系统竞争
重要的区别在于绕过了明确的安全控制。
这不仅仅是:
监视文件变更属于预期行为。
真正的问题在于:
这是信任边界的直接失效。
应用程序此前已经做出了安全决策:
- 该目标位于沙箱内
- 因此可以安全地注册并获取
但随后的获取并未绑定到证明该决策合理的目标上。
这就是将普通的文件系统可变性转变为漏洞的原因。
## 概念验证
我围绕确切的求值和依赖项获取序列构建了一个独立的复现程序。
受控的设置包括:
- 一个已配置的沙箱目录
- 该沙箱内的一个安全文件
- 沙箱外的一个机密文件
- 沙箱内的一个受监视的符号链接路径
- 在依赖项获取前后进行确定性的链接替换
复现流程如下:
1. 将受监视的符号链接指向沙箱内的安全文件。
2. 求值 `file` 辅助函数,使得沙箱验证成功并将原始路径注册为依赖项。
3. 在依赖项获取之前,将受监视的符号链接重定向至外部的机密文件。
4. 运行依赖项获取,并允许 `os.ReadFile(d.path)` 通过重定向后的链接进行读取。
5. 将获取的值存储在模板缓存中。
6. 将符号链接恢复为沙箱内的安全文件。
7. 再次对模板进行求值。
8. 确认沙箱验证仍然成功。
9. 确认渲染出的输出包含先前获取的外部机密信息。
观察到的行为是:
- 初始的沙箱验证通过
- 依赖项保留了原始的链接路径
- 在链接更改后,获取操作读取了沙箱外的文件
- 在下一次渲染前恢复了安全目标
- 下一次沙箱检查通过
- 渲染的输出在 SHA-256 上与外部机密信息完全匹配
这种哈希比对至关重要。
它证明了最终渲染的值不是过期的安全内容、文件名构件或错误路径的副作用。
它是沙箱外文件的逐字节精确内容。
## 为什么选择这种 PoC 方式
对于 TOCTOU 问题,最有力的证明必须控制时间线。
仅仅展示符号链接可以指向沙箱外的证明力度较弱,因为 `pathInSandbox()` 在观察到外部目标时已经对其进行了拒绝。
该安全声明依赖于按顺序证明所有这些状态:
- 验证时是安全的
- 获取时是不安全的
- 渲染时再次变为安全的
- 外部内容仍从缓存中被消耗
这就是为什么该复现程序显式地将模板验证、依赖项获取、链接恢复和下一次渲染分离开来的原因。
该 PoC 不依赖于随机时间或重复猜测。
它以确定性方式驱动了易受攻击的生命周期。
这使得根本原因和影响变得更容易进行辩护。
## 利用要求与影响范围
此问题需要本地文件系统的影响力。
攻击者需要足够的访问权限,以便在漏洞窗口期创建、替换或重新定向相关的符号链接或等效的链接路径。
Consul Template 进程还必须:
- 拥有读取外部目标的权限
- 使用受攻击者影响的路径对模板进行求值
- 将渲染结果暴露在攻击者可以恢复或影响其使用的位置
这些要求非常重要。
这不是任何默认部署中未经身份验证的远程任意文件读取。
但在受影响的本地信任模型内,其影响是有意义的:
- 绕过了文档记载的 `sandbox_path` 限制
- 导致沙箱外的本地文件泄露
- 可能暴露 Consul Template 进程可读取的机密信息
当进程拥有访问敏感凭据、服务配置、令牌或私钥的权限时,风险会增加。
## 严重程度与分类
HashiCorp 将此问题分类为:
- **CWE-59**: 文件访问前的链接解析不当
- **CVSS 3.1:**
```
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)
```
安全属性从:
- 验证解析后的目标
- 丢弃解析后的目标
- 稍后通过原始路径获取
变为:
- 验证解析后的目标
- 保留解析后的目标
- 通过已验证的路径进行获取
这关闭了报告的符号链接重定向路径,因为更改原始链接不再会更改依赖项存储的路径。
该补丁还添加了一个专门的回归测试,涵盖了确切的序列:
- 验证期间为安全符号链接
- 获取期间为外部符号链接
- 在下一次调用前恢复安全符号链接
- 不得返回缓存的外部机密
这正是你希望针对此类漏洞采取的补救措施:
- 修复失效的验证/使用绑定
- 保留沙箱行为
- 为完整的竞争生命周期添加回归测试
## 披露
我于 **2026 年 3 月 20 日** 向 HashiCorp Security 私下报告了此问题。
报告包含:
- 源代码级别的根本原因分析
- 从验证到获取的 TOCTOU 序列
- 独立的确定性复现程序
- 运行时输出
- 显示渲染数据与外部机密相匹配的 SHA-256 证据
- 受影响的修订版本详细信息
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` 再次解析了该路径
- 恢复安全链接并未移除已缓存的外部内容
- PoC 通过 SHA-256 证明了逐字节精确的沙箱外泄露
- `0.42.0` 版本通过将依赖项绑定到已解析、已验证的路径修复了该漏洞
## 结语
此漏洞不在于绕过字符串前缀检查。
它关乎时间与身份。
Consul Template 在模板求值期间检查了链接的指向。
依赖项获取随后又向文件系统再次询问了同一个问题。
攻击者可以在这两个时刻之间改变答案。
这就是它成为 **CVE-2026-5061** 的原因。
已在 **consul-template 0.42.0** 中修复。
标签:HashiCorp, 安全漏洞, 日志审计, 模板引擎, 沙箱逃逸, 漏洞分析, 符号链接攻击, 路径探测