- 云原生
【免费下载链接】buildah
A tool that facilitates building OCI images.
本文围绕 buildah 项目(go.podman.io/buildah)中实际 vendored 的第三方库vendor/github.com/cyphar/filepath-securejoin展开,系统讲解其在 OCI 镜像构建工具中承担的安全路径解析职责:从旧的SecureJoin语义、其固有 TOCTOU 风险与四条安全保证,到基于openat2/fsopen/open_tree等现代内核 API 的全新OpenInRoot/MkdirAll接口,并结合 buildah 源码与测试验证其真实调用场景。读完本文,你将理解容器工具链中“把用户输入路径安全地限制在 root 目录内”的完整方案演进,并掌握新旧两套 API 的适用边界与正确用法。
一、背景:为什么容器构建工具需要“安全的 filepath.Join”
在 buildah 这类 OCI 镜像构建工具中,存在大量“以用户输入路径在某个上下文目录(context directory)内查找/创建文件”的场景。例如解析构建上下文中的.containerignore/.dockerignore文件、从 git/HTTP 下载的构建上下文里定位子目录等。如果这些路径直接交给标准的filepath.Join拼接,恶意输入中的..或符号链接就可能把访问引导到 root 目录之外,造成路径逃逸。
filepath-securejoin最初就是作为SecureJoin的实现而诞生的,其设计意图是成为 Go 标准库中一个更安全的filepath.Join,将路径查找限制在某个 root 目录之内(该提案原计划进入 Go 标准库,见go#20126)。实现思路源自多个容器运行时中已有的代码。在 buildah 中,该库被真实用于这些高风险路径解析点:
- pkg/parse/parse.go 的
ContainerIgnoreFile函数中,用securejoin.SecureJoin(contextDir, ".containerignore")与securejoin.SecureJoin(contextDir, ".dockerignore")安全定位 ignore 文件,防止构建上下文中的符号链接把解析引出上下文目录; - pkg/tmpdir/url.go 解析 git / HTTP 构建上下文时,用
securejoin.SecureJoin(downloadDir, contentSubdir)定位下载目录内的子目录; - copier/copier_test.go 与 copier/copier_test.go 的测试用例同样借助
SecureJoin构造安全的期望路径。
二、旧 API:SecureJoin/SecureJoinVFS的语义与保证
2.1 函数形态与核心行为
旧 API 的核心是SecureJoin(以及支持注入虚拟文件系统接口的SecureJoinVFS,其实现位于vendor/github.com/cyphar/filepath-securejoin/join.go):
func SecureJoin(root, unsafePath string) (string, error)它把unsafePath与root安全地连接起来,返回的路径保证落在root之内。SecureJoinVFS则额外接受一个 VFS 接口,nil 时回落到标准os.*函数族——这允许在不实际触碰文件系统的情况下模拟路径解析,便于测试。
2.2 四条安全保证
文档明确给出了该库在旧 API 下的四条保证,任何使用SecureJoin的代码都应以此为准:
- 子路径保证:只要未返回错误,结果字符串必然是
root的子路径,且其中不再包含任何符号链接路径组件(全部已被展开)。 - chroot 语义:展开符号链接时,所有符号链接组件必须相对于所提供的 root 解析——这可以视为
chroot(2)处理文件路径方式的用户态实现。注意这些符号链接不会被词法展开(处理前不会先调用filepath.Clean)。 - 不存在的组件不受影响:与
filepath.EvalSymlinks语义类似,SecureJoin不会因为路径中某个组件不存在而失败,而是将其原样保留(允许悬空符号链接的部分解析)。 - 结果必然被 Clean:返回路径总是经过
filepath.Clean,因此不包含任何..组件。
2.3 一个“平凡但危险”的对照实现
为帮助理解语义,README 给出了一个 GNU/Linux 上使用chroot+readlink的对照实现(需要 root 权限,且要求root内的readlink可信任,比库内实现更晦涩):
package securejoin import ( "os/exec" "path/filepath" ) func SecureJoin(root, unsafePath string) (string, error) { unsafePath = string(filepath.Separator) + unsafePath cmd := exec.Command("chroot", root, "readlink", "--canonicalize-missing", "--no-newline", unsafePath) output, err := cmd.CombinedOutput() if err != nil { return "", err } expanded := string(output) return filepath.Join(root, expanded), nil }这段代码形象地说明了“以 root 为文件系统根展开符号链接”的含义,同时也暴露了它的性能与信任成本——这也是库内实现(纯用户态、逐组件解析)存在的意义。
2.4 根本缺陷:TOCTOU 竞态
README 中最重要的警告是:旧 API 在安全模型上“从根本上是危险的”。SecureJoin返回一个字符串路径,此后调用者再对该路径执行os.OpenFile/os.MkdirAll等操作。如果攻击者能在SecureJoin返回之后、调用者使用该路径之前修改路径中的某个组件(例如把某个中间目录替换成符号链接),就会出现典型的 TOCTOU(time-of-check to time-of-use)竞态,导致路径逃逸。
因此:
SecureJoin(及SecureJoinVFS)仍被提供,仅用于支持历史遗留用户;- 新用户被强烈建议避免使用
SecureJoin,改用下文的新 API,或迁移到 libpathrs。
三、新 API:基于内核原语的竞态免疫方案
新 API 是 libpathrs 部分方法的移植,仅支持 Linux,其核心思路是放弃“返回字符串路径”,改为在打开文件描述符的过程中完成全部校验与解析,从机制上消灭 TOCTOU 窗口。
3.1 底层内核 API 支撑
新 API 的实现会机会性地使用更新的内核接口(底层实现见vendor/github.com/cyphar/filepath-securejoin/open.go、vendor/github.com/cyphar/filepath-securejoin/mkdir.go以及pathrs-lite/子包):
openat2(2)(Linux 5.6+):所有查找操作都使用openat2,以限制 magic-links 与 bind-mount 穿越(针对部分操作),并利用RESOLVE_IN_ROOT在 rootfs 内高效解析符号链接。fsopen(2)/open_tree(2)(Linux 5.2+):为所有用户提供针对恶意/proc挂载的加固——要么检测出伪造的/proc,要么避免被其欺骗。普通用户通过openat2获得保护,特权用户额外获得fsopen/open_tree的更深层防护。
3.2OpenInRoot:安全的打开句柄
func OpenInRoot(root, unsafePath string) (*os.File, error) func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) func Reopen(handle *os.File, flags int) (*os.File, error)OpenInRoot是对下面这段易受攻击代码的“更安全版本”:
path, err := securejoin.SecureJoin(root, unsafePath) file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)新 API 之所以安全,是因为它把“解析路径 + 打开文件”合并为一次不可被中间篡改的内核操作。使用要点:
- 返回的
*os.File是O_PATH文件描述符,能力受限;调用者通常需要配合Reopen把它转成更可用的句柄。这个拆分是有意为之:既支持 PTY 生成等高级场景,也避免用户意外打开坏 inode 造成 DoS。 - 调用者必须小心使用返回的句柄——通常只能直接对句柄操作,稍有不慎就会制造新的安全问题。libpathrs 提供了更多辅助函数来安全使用这类句柄,但当前没有计划把它们移植回
filepath-securejoin。 OpenatInRoot与OpenInRoot的区别在于 root 通过*os.File提供,从而保证多次OpenatInRoot(或MkdirAllHandle)调用作用于同一个 rootfs。
注意:与
SecureJoin不同,OpenInRoot一旦遇到悬空符号链接或不存在的路径就立即报错。SecureJoin会把不存在的组件当作真实目录、允许悬空符号链接的部分解析;这两种行为与 Linux 对不存在路径和悬空符号链接的处理方式相悖,因此新 API 不再允许。
3.3MkdirAll:安全的递归建目录
func MkdirAll(root, unsafePath string, mode int) error func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)MkdirAll是对下面这段易受攻击代码的“更安全版本”,防护的竞态与OpenInRoot相同:
path, err := securejoin.SecureJoin(root, unsafePath) err = os.MkdirAll(path, mode)MkdirAllHandle与MkdirAll的关系如同OpenatInRoot与OpenInRoot:root 以*os.File提供,并返回最终创建目录的*os.File。该句柄保证与MkdirAllHandle创建的目录“实际等同”——这是先用MkdirAll再OpenatInRoot无法保证的。- 同样的 NOTE 适用:一旦遇到悬空符号链接或不存在路径即报错,不会为悬空符号链接所引用的不存在目录创建目录。
四、在 buildah 中的实际调用链
buildah 当前对filepath-securejoin的引入集中在路径解析与校验场景,全部走旧 APISecureJoin:
- 构建上下文 ignore 文件定位:pkg/parse/parse.go 在
ContainerIgnoreFile中依次对.containerignore、.dockerignore执行securejoin.SecureJoin(contextDir, ...),解析失败即返回错误;随后将结果交给imagebuilder.ParseIgnore读取排除规则。该函数在注释中被标记为“已弃用,可能转为 internal”,说明 buildah 正在谨慎收敛对旧 API 的依赖。 - 远程构建上下文子目录解析:pkg/tmpdir/url.go 在 git clone / HTTP 下载 / stdin 等上下文落地到
downloadDir后,用securejoin.SecureJoin(downloadDir, contentSubdir)解析用户指定的子目录,出错时报 “resolving subdirectory …” 错误。 - 测试验证:copier/copier_test.go 与 copier/copier_test.go 在 copier 的路径安全测试中,用
SecureJoin(tmpdir, ...)构造期望路径,与解析结果比对,间接验证了符号链接、..等输入下的行为符合库的保证。
从代码结构看,buildah 的引入方式符合“旧 API 用于无文件系统竞争的传统路径校验,新 API 面向需要抗竞态的打开/创建操作”的分工;对安全性要求更高的打开与建目录场景,可依据本库的演进方向逐步迁移到OpenInRoot/MkdirAll。
五、许可证与再分发说明
本库的许可证标识为SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0,双许可证并存:
- 部分代码衍生自 Go 标准库,采用BSD 3-clause许可(见 vendor/github.com/cyphar/filepath-securejoin/LICENSE.BSD);
- 其余文件(多数衍生自 libpathrs)采用MPL-2.0(见 vendor/github.com/cyphar/filepath-securejoin/LICENSE.MPL-2.0)。使用上文“新 API”的用户,多半在使用此许可证下的代码;
- 每个源文件头部都有版权声明标明适用许可证,请逐文件核对;更多细节可参考 vendor/github.com/cyphar/filepath-securejoin/COPYING.md(仓库内的完整副本见同目录)。
六、结论与选型建议
- 追求兼容性、且调用点不存在“解析后复用”竞态窗口(如 buildah 中纯路径校验场景),可继续使用
SecureJoin,但必须清楚其 TOCTOU 局限,并遵守四条保证的边界; - 需要真正抗竞态的文件打开 / 目录创建,应使用新 API:
OpenInRoot/OpenatInRoot/Reopen与MkdirAll/MkdirAllHandle,并接受“遇到悬空符号链接或缺失路径立即报错”的严格语义; - 长期路线:README 建议用户迁移到 libpathrs,本库新 API 正是为平滑过渡而移植的过渡层。
依赖该库的构建工具在升级内核(Linux 5.6+ 支持openat2,5.2+ 支持fsopen/open_tree)后,将自动获得基于新内核能力的加固路径解析,这也是 OCI 构建工具链在路径安全上的主流演进方向。
- 云原生
【免费下载链接】buildah
A tool that facilitates building OCI images.
相关推荐
OpenCloud 依赖解析:filepath-securejoin 安全路径库的旧版 API 局限与新 API 实战
OpenCloud 依赖解析:filepath securejoin 安全路径库的旧版 API 局限与新 API 实战 导读 filepath securejo
后端微服务存储认证鉴权skopeo 依赖解析:filepath-securejoin 安全路径连接库的原理、API 与源码实践
skopeo 依赖解析:filepath securejoin 安全路径连接库的原理、API 与源码实践 本文以 skopeo 仓库中 vendored 的 g
云原生CLI镜像仓库KubeEdge 依赖剖析:filepath-securejoin 安全路径拼接库的 API 演进与容器场景实践
KubeEdge 依赖剖析:filepath securejoin 安全路径拼接库的 API 演进与容器场景实践 filepath securejoin 是一个
云原生边缘计算物联网容器编排边缘网关
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考