cyphar/filepath-securejoin
GitHub: cyphar/filepath-securejoin
Go 语言安全路径操作库,提供防符号链接穿越和 TOCTOU 攻击的文件路径解析能力,主要服务于容器运行时等需要安全文件访问的场景。
Stars: 112 | Forks: 23
## `filepath-securejoin`
[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
### 旧版 API
该库最初只是 `SecureJoin` 的一个实现,其目的是[计划包含在 Go 标准库中][go#20126],作为一种更安全的 `filepath.Join`,它可以将路径查找限制在根目录内。
该实现基于几个容器 runtime 中已有的代码。不幸的是,该 API **从根本上是不安全的**,无法防范那些能够在 `SecureJoin` 返回之后、调用者使用该路径之前修改路径组件的攻击者,这允许发起一些相当简单的 TOCTOU 攻击。
该库仍然提供 `SecureJoin`(和 `SecureJoinVFS`)以支持旧版用户,但强烈建议新用户避免使用 `SecureJoin`,而是使用[新版 API](#new-api) 或切换到 [libpathrs][libpathrs]。
考虑到上述限制,该库保证以下几点:
* 如果没有返回错误,生成的字符串**必须**是 `root` 的子路径,并且不包含任何符号链接路径组件(它们都会被展开)。
* 在展开符号链接时,所有符号链接路径组件**必须**相对于提供的根目录进行解析。特别地,这可以被视为 `chroot(2)` 如何操作文件路径的用户空间实现。请注意,这些符号链接**不会**从词法上展开(在处理之前不会对输入调用 `filepath.Clean`)。
* 不存在的路径组件不受 `SecureJoin` 的影响(类似于 `filepath.EvalSymlinks` 的语义)。
* 返回的路径将始终经过 `filepath.Clean` 处理,因此不包含任何 `..` 组件。
在 GNU/Linux 系统上,该函数的一个(简单的)实现可以通过以下方式完成(请注意,这需要 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
}
```
### 新版 API
虽然我们建议用户在 [libpathrs][libpathrs] 一发布稳定版本后尽快切换,但 libpathrs 实现的一些方法已经被移植到该库中,以简化过渡过程。这些 API 仅在 Linux 上受支持。
这些 API 的实现方式使得 `filepath-securejoin` 能够视情况使用某些更新的内核 API,从而使这些操作变得安全得多。特别是:
* 所有的查找操作都会在足够新的内核(Linux 5.6 或更高版本)上使用 [`openat2`][openat2.2],以限制通过 magic-links 和 bind-mounts(对于某些操作)进行查找,并利用 `RESOLVE_IN_ROOT` 高效地解析 rootfs 内的符号链接。
* 这些 API 提供了针对恶意 `/proc` 挂载的强化保护,以检测或避免被不合法的 `/proc` 欺骗。这是通过为所有用户使用 [`openat2`][openat2.2] 来完成的,而特权用户还将通过使用 [`fsopen`][fsopen.2] 和 [`open_tree`][open_tree.2](Linux 5.2 或更高版本)获得进一步的保护。
#### `OpenInRoot`
```
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)
```
的一个更安全的版本,它可以防范可能导致严重安全问题(具体取决于应用程序)的各种竞态攻击。请注意,返回的 `*os.File` 是一个 `O_PATH` 文件描述符,其限制非常多。调用者可能需要使用 `Reopen` 来获取更可用的句柄(这种拆分是为了提供 PTY 生成等实用功能,并避免用户意外打开可能导致 DoS 的不良 inode)。
调用者在使用返回的 `*os.File` 时需要小心。通常,直接操作该句柄才是安全的,而且很容易引发安全问题。[libpathrs][libpathrs] 提供了更多的辅助函数来让这些句柄的使用变得更安全——目前没有计划将它们移植到 `filepath-securejoin`。
`OpenatInRoot` 类似于 `OpenInRoot`,区别在于根目录是通过 `*os.File` 提供的。这允许你确保多个 `OpenatInRoot`(或 `MkdirAllHandle`)调用是在同一个 rootfs 上操作的。
#### `MkdirAll`
```
func MkdirAll(root, unsafePath string, mode int) error
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
```
`MkdirAll` 是
```
path, err := securejoin.SecureJoin(root, unsafePath)
err = os.MkdirAll(path, mode)
```
的一个更安全的版本,它可以防范 `OpenInRoot` 所防范的那些竞态问题。
`MkdirAllHandle` 类似于 `MkdirAll`,区别在于根目录是通过 `*os.File` 提供的(其原因与 `OpenatInRoot` 相同),并且会返回最终创建目录的 `*os.File`(保证该目录与 `MkdirAllHandle` 创建的目录实际上是相同的,而这仅通过在 `MkdirAll` 之后使用 `OpenatInRoot` 是无法确保的)。
### 许可证
`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
本项目中的部分代码派生自 Go,并在 BSD 3-clause 许可证下授权(详见 `LICENSE.BSD`)。其他文件(其中许多派生自 [libpathrs][libpathrs])在 Mozilla Public License 2.0 版本下授权(详见 `LICENSE.MPL-2.0`)。如果你使用的是[上述“新版 API”][#new-api],你很可能正在使用根据该许可证发布的文件中的代码。
本项目中的每个源文件都有一个描述其许可证的版权头部。请检查每个文件的版权头部,以查看适用于它的许可证。
有关更多详细信息,请参阅 [COPYING.md](./COPYING.md)。
标签:EVTX分析, 日志审计