ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

filepath-securejoin 深度指南:在 Go 中安全解析容器 rootfs 内文件路径

filepath-securejoin 深度指南:在 Go 中安全解析容器 rootfs 内文件路径 测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载filepath-securejoin是一个为容器运行时Docker、runc、Kubernetes 等生态设计的 Go 路径解析库其核心目标是让开发者像在chroot(2)环境中那样把任意不可信路径安全地解析到一个 rootfs 根目录之内从而抵御符号链接逃逸攻击。本文以当前仓库 vendor 目录中的 v0.6.1 源码与文档为据系统讲解其旧版 APISecureJoin/SecureJoinVFS与新 APIpathrs-lite的OpenInRoot、MkdirAll等的设计动机、安全保证、源码实现与实战用法读完你既能写出可用的安全路径解析代码也能理解为什么官方强烈建议弃用旧 API。库定位与背景为什么拼接路径在容器场景下不安全在容器运行时中filepath.Join(root, unsafePath)这类朴素拼接是危险的unsafePath中可能携带..组件或包含指向 rootfs 之外的符号链接导致最终打开的路径逃逸出容器根目录。filepath-securejoin最初就是为此而生——它的作者曾尝试将SecureJoin合入 Go 标准库对应 Go issue 20126最终被拒绝后以独立库形式维护至今。正如库内 doc.go 所述它已经被 Docker、runc、Kubernetes 等多个容器运行时作为操作容器文件系统路径的事实标准使用了多年。值得注意的对比是Go 标准库后来加入了os.Root但其设计目标是openat2(RESOLVE_BENEATH)语义并不完全契合容器运行时与大多数系统工具的 rootfs 解析场景因此filepath-securejoin依然有独立价值。在当前仓库中它作为 OpenShift 项目的间接依赖github.com/cyphar/filepath-securejoin v0.6.1 // indirect被引入随 Kubernetes 等上游一并 vendored 到 vendor/github.com/cyphar/filepath-securejoin 目录。旧 APISecureJoin与SecureJoinVFS旧 API 是本库的原始形态也是最广为人知的部分。核心入口在 join.gofunc SecureJoin(root, unsafePath string) (string, error) func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error)四条核心安全保证README 明确给出了SecureJoin的四条语义保证理解它们是正确使用的前提结果必须落在 root 之下只要不返回错误返回字符串必然是root的子路径且其中不包含任何符号链接组件所有链接都已被展开。符号链接一律相对于 root 解析展开过程中所有符号链接组件必须以传入的 root 为根进行解析这相当于在用户态实现chroot(2)对文件路径的处理方式。注意这些链接不会被词法展开——处理前不会调用filepath.Clean。不存在的路径组件不受影响语义与filepath.EvalSymlinks一致SecureJoin会把不存在的组件当作普通目录继续处理。结果始终是 Clean 的返回路径必然经过filepath.Clean不会残留..组件。必须知晓的根本缺陷TOCTOU 竞态README 用非常直白的措辞警告旧 API 面对能够在SecureJoin返回之后、调用方使用路径之前修改路径组件的攻击者是从根本上不安全的容易引发 TOCTOUtime-of-check to time-of-use攻击。原因在于你无法返回一个安全的路径字符串并同时保证它在之后不会被文件系统上的符号链接替换——攻击者只需在两次操作之间把某个中间目录换成指向 rootfs 之外的软链接即可。因此SecureJoin以及SecureJoinVFS仍被提供是为了支持遗留用户新用户被强烈建议不要使用它们而应转向下文的新 API 或 libpathrs。源码级实现解析join.go 中SecureJoinVFS的算法可以概括为一次逐组件循环展开校验 root若 root 包含..组件通过hasDotDot检测直接返回errUnsafeRoot错误因为非词法规范的 root 会带来难以预期的拼接结果。官方还建议 root 应先用filepath.EvalSymlinks完全解析、且绝不能由攻击者控制。逐组件切分把unsafePath按分隔符切成单个组件每次对当前已构建路径做一次词法filepath.Join。Lstat 判定用 VFS 的Lstat检查完整路径若组件不存在或不是符号链接就按普通目录继续推进。展开符号链接若命中符号链接则用Readlink读取目标把目标内容前插到未解析的剩余路径之前继续解析若目标是绝对路径则重置已构建的currentPath。同时用MaxSymlinkLimit限制链接展开次数超出即返回ELOOP错误防止符号链接环导致死循环。可替换文件系统的 VFS 接口SecureJoinVFS的第三参用于把文件系统状态抽象出来便于 mock 测试或接入 VFS 类系统。接口定义在 vfs.go只有两个方法语义分别对齐os.Lstat与os.Readlinktype VFS interface { Lstat(name string) (os.FileInfo, error) Readlink(name string) (string, error) }传nil时等价于使用标准os.*函数族内部用osVFS包装。SecureJoin本身只是SecureJoinVFS(root, unsafePath, nil)的薄封装。GNU/Linux 上的朴素实现chroot readlinkREADME 给出了一个教学性质的对照实现——它借助系统chroot与readlink --canonicalize-missing达到相似效果但需要 root 权限、实现更晦涩且要求readlink二进制位于 root 路径内且可信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 之内完成链接解析的核心思想而本库的纯 Go 实现并不需要 root 权限可读性与可移植性都更强。新 APIpathrs-lite仅 Linux为了平滑过渡到 libpathrs本库把 libpathrs 的若干方法以纯 Go形式移植进了 pathrs-lite 子包。这些 API只在 Linux 上支持且被设计为机会主义地利用更新的内核接口来获得远高于旧 API 的安全性。底层内核能力README 与源码共同确认了以下内核利用策略所有查找操作在Linux 5.6 及以上使用openat2(2)file, err : openat2(root, unsafePath, unix.OpenHow{ Flags: unix.O_PATH | unix.O_CLOEXEC, Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS, })针对恶意/proc挂载提供加固所有用户都经由openat2(2)规避欺骗而特权用户还会借助fsopen(2)与open_tree(2)Linux 5.2 及以上获得额外保护。OpenInRoot/OpenatInRoot/Reopenfunc 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)朴素版本不安全的原因正如上文所述攻击者可以在SecureJoin与os.OpenFile之间篡改文件系统使最终打开的文件落在 root 之外。而OpenInRoot在单次内核调用中完成解析与打开从根上消除该竞态窗口。从 open.go 的实现可以看到其入口流程先用os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)把 root 打开为目录句柄再转交OpenatInRoot完成剩余工作。使用上有两点必须注意返回的是O_PATH文件描述符能力极其受限——这是刻意设计一方面支持 PTY 生成等特殊场景另一方面避免用户意外打开坏 inode 造成 DoS。要获得可用的句柄通常需要借助Reopen升级。调用方必须谨慎使用返回的*os.File通常只应直接基于句柄操作且很容易制造出安全隐患。libpathrs 提供了远更丰富的安全操作辅助函数而本库目前没有计划将其移植过来。OpenatInRoot与OpenInRoot的区别仅在于以*os.File提供 root从而保证多次OpenatInRoot或MkdirAllHandle调用都作用在同一个 rootfs上避免路径与句柄错位。Reopen则是通过/proc/self/fd以指定 flags 重新打开句柄等价于fdPath : fmt.Sprintf(/proc/self/fd/%d, file.Fd()) os.OpenFile(fdPath, flags|unix.O_CLOEXEC)区别在于Reopen额外做了加固防止被恶意配置的/proc挂载所欺骗——这正是 CVE-2019-19921 所描述的容器运行时攻击场景见 open_libpathrs.go 的注释。NOTE与SecureJoin不同OpenInRoot一旦遇到悬空符号链接或不存在的路径就会立即报错。这是与SecureJoin相反的语义——旧 API 把不存在的组件当作真实目录继续解析、允许悬空链接的部分解析但这两个行为与 Linux 对不存在路径和悬空链接的真实处理方式相悖因此新 API 不再允许。MkdirAll/MkdirAllHandlefunc MkdirAll(root, unsafePath string, mode os.FileMode) error func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error)MkdirAll是下面这段朴素代码的安全版本path, err : securejoin.SecureJoin(root, unsafePath) err os.MkdirAll(path, mode)朴素版本的缺陷在于攻击者若能在SecureJoin与os.MkdirAll之间篡改文件系统MkdirAll就可能解析到不安全的符号链接组件把目录创建到 root 之外。MkdirAll防住的正是这类竞态——即便攻击者把目录从 root 内移到 root 外也保证我们创建目录树的每一步都不会走出正在创建的目录树。源码层面mkdir.go 中的MkdirAll同样是先打开 root 目录句柄然后委托给MkdirAllHandle并关闭返回的句柄func MkdirAll(root, unsafePath string, mode os.FileMode) error { rootDir, err : os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err ! nil { return err } defer rootDir.Close() f, err : MkdirAllHandle(rootDir, unsafePath, mode) if err ! nil { return err } _ f.Close() return nil }MkdirAllHandle除了同样接受*os.File形式的 root 之外还会返回最终创建目录的*os.File句柄——这个句柄保证与MkdirAllHandle创建的目录实际等同这是先MkdirAll再OpenatInRoot无法确保的。NOTE与OpenInRoot同理MkdirAll遇到悬空符号链接或不存在的路径会立即报错不会为悬空符号链接所指向的不存在目录创建任何目录。不存在的路径如何被优雅处理一个值得展开的实现细节当目标路径的某些前缀尚不存在时openat2(RESOLVE_IN_ROOT)会直接失败。为此 openat2_linux.go 提供了partialLookupOpenat2从完整路径逐级向上缩短前缀重试找到最深的存在组件作为锚点句柄并把剩余路径交还调用方继续处理如逐级mkdir。这样既保持了RESOLVE_IN_ROOT的安全边界又保留了创建尚不存在目录树的能力。随时可切换的 libpathrs 后端pathrs-lite并不要求你一直用纯 Go 实现。按 pathrs-lite/README.md 的说明构建时只要打上libpathrsbuild tagpathrs-lite就会直接使用 libpathrsCGo而放弃纯 Go 后端见带//go:build libpathrs的 open_libpathrs.go。两个后端功能等价并有集成测试验证因此这条迁移路径对下游用户几乎是零感知的——这也是官方推荐终态迁移到 libpathrs 的过渡台阶。新旧 API 选型速查关注点旧 APISecureJoin/SecureJoinVFS新 APIOpenInRoot/MkdirAll等竞态TOCTOU防护无返回字符串后无法保证有单次内核调用完成解析操作悬空链接/不存在路径当作普通目录继续解析立即报错语义与 Linux 一致平台支持跨平台仅 Linux底层机制逐组件Lstat/Readlink循环openat2(RESOLVE_IN_ROOT)、fsopen/open_tree返回形态路径字符串O_PATH句柄需Reopen升级官方建议仅限遗留用户新用户首选长期迁移 libpathrs许可证说明本库采用双许可证策略SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0。部分源自 Go 的代码采用 BSD 3-clause 许可见 LICENSE.BSD其余文件许多源自 libpathrs采用 Mozilla Public License 2.0见 LICENSE.MPL-2.0。如果你使用上文所述的新 API基本就是在使用该许可下的代码。每个源文件头部都标注了适用的许可证使用前请逐文件核对更详细的说明见 COPYING.md。结语从追求返回一个安全的路径字符串的SecureJoin到以openat2句柄为中心的OpenInRoot/MkdirAllfilepath-securejoin的演进折射出容器安全路径解析领域的范式转变安全不应依赖路径文本而应锚定已打开的内核句柄。对 OpenShift 这类基于 Kubernetes 的容器平台而言理解 vendor 中这份依赖的实现机理有助于在审查上游容器运行时代码、排查路径逃逸类问题时快速定位风险点——而当你需要在自己编写的 Go 代码中安全地操作容器 rootfs 时本库 v0.6.1 的文档与源码README、join.go、pathrs-lite 子包就是最直接的参考资料。赞分享测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载相关推荐filepath-securejoin 安全路径解析库演进深度解析从 SecureJoin 到 pathrs-lite 的容器安全之路filepath securejoin 安全路径解析库演进深度解析从 SecureJoin 到 pathrs lite 的容器安全之路 本文以 core/ve机器学习深度学习数据可视化可观测性Go 安全路径解析库 filepath-securejoin 深度解析从 SecureJoin 到基于 openat2 的 pathrs-liteGo 安全路径解析库 filepath securejoin 深度解析从 SecureJoin 到基于 openat2 的 pathrs lite filep云原生网络服务网格可观测性网络安全eBPFfilepath-securejoin 演进全解析从 SecureJoin 到 pathrs-lite 的容器安全路径解析之路filepath securejoin 演进全解析从 SecureJoin 到 pathrs lite 的容器安全路径解析之路 导读 github.com/c云原生上一篇G-Helper免费轻量级华硕笔记本控制完全指南3分钟告别 Armory Crate下一篇笔记效率提升10倍Foam自定义模板让重复工作彻底自动化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表