你依赖树中的任何一个 crate 都可以打开 /etc/passwd、向你的 ~/.ssh 目录写入内容,或者枚举你项目中的每一个文件。Rust 的标准库不会请求许可。它假设任何能够调用 std::fs::File::open 的代码都有权接触操作系统允许的任何路径。
cap-std 改变了这一假设。它是 Rust std I/O module 的直接替代品,用显式权能取代了隐式权限。如果你想打开一个文件,你首先需要一个权能来证明你有权访问包含该文件的目录。
这比你想象的更重要。供应链攻击并不总是需要复杂的漏洞利用。有时它们只需要一个构建脚本或一个传递依赖,在你运行测试时悄悄地将文件外传。在库级别对此风险进行沙箱化,而无需容器或虚拟机,这正是 cap-std 所实现的。
基于权能的文件系统访问实际看起来是什么样的
在标准 Rust 中,打开文件是一个零前提条件的一行代码:
use std::fs::File;
// Any code, anywhere, can do this
let file = File::open("/etc/passwd")?;
路径是绝对的,权限是隐式的。操作系统会检查权限,但程序本身从不需要证明它本应能够构造出该路径。
cap-std 将其替换为两步模型。你从一个权能开始,通常是一个目录句柄,并且你只能打开相对于该目录的路径:
use cap_std::fs::Dir;
use std::path::Path;
// Open the current working directory as a capability
let cwd = Dir::open_ambient_dir(".", cap_std::ambient_authority())?;
// Now we can only open files inside this directory tree
let file = cwd.open("config/app.toml")?;
注意 open_ambient_dir 调用。那是一个逃生出口。它将一个隐式路径转换为权能,并且被故意设计得冗长且易于审计。一旦你有了 Dir,就没有接受绝对路径的 open 方法。API 根本不允许这样做。
cap-std 如何在不复制其安全模型的情况下镜像 std
cap-std 被构造为 std::fs、std::net 和 std::os::unix::net 的直接替代品。这些类型被刻意设计得与标准库相似。cap_std::fs::File 包装了 std::fs::File。cap_std::fs::Dir 是新的入口点,大致类似于在 chroot 中工作,只不过是由类型系统而非特权操作系统调用来强制执行的。
该 crate 通过一个名为 cap-primitives 的底层抽象来实现这一点。在底层,cap-std 在 Unix 上使用 openat 风格的系统调用,在 Windows 上使用类似的受限相对操作。它从不调用标准库的不受限制的 File::open。在 Linux 上,当可用时,它使用带有 RESOLVE_BENEATH 的 openat2 来防止符号链接逃逸。在 Windows 上,它使用带有受限标志的 NtCreateFile。
这不是容器或 seccomp 的包装器。它是标准文件系统 API 的重新实现,只是省略了危险的操作。
Dir 类型支持你所期望的大部分功能:open、create_dir、rename、remove_file、read_dir 等等。关键区别在于每个操作都是相对于该目录句柄的。如果你想将文件移出树外,你需要两个 Dir 权能,一个用于源目录,一个用于目标目录。API 迫使你随身携带访问证明。
cap-std 使用的可移植性技巧
文件系统权能模型因操作系统而异。Linux 有 openat2。FreeBSD 有 cap_rights_limit 和 O_RESOLVE_BENEATH。Windows 有基于句柄的访问控制,但没有 RESOLVE_BENEATH 的直接等价物。macOS 是主要平台中支持最有限的。
cap-std 通过一个可移植层来处理这一点。在具有强内核支持的系统上,它使用原生限制机制。在没有这些支持的系统上,它退回到用户空间实现,手动解析符号链接并验证相对路径的每个组件都保持在权能边界内。
这种回退更昂贵,但它意味着你的安全模型是可移植的。WASI 沙箱、Linux 服务器和开发者的 MacBook 都可以强制执行相同的权能边界,而无需应用程序中的平台特定代码。
在真实项目中使用 cap-std 是什么样的
迁移现有项目没有你想象的那么痛苦,因为 API 被故意设计得接近 std。主要的变化是你不再将 &Path 或 &str 作为文件系统路径单元到处传递。你改为传递 &Dir。
这里是一个从调用方控制的目录读取配置的简化示例:
use cap_std::fs::Dir;
use std::io::{self, Read};
pub fn load_config(dir: &Dir, name: &str) -> io::Result<String> {
let mut file = dir.open(name)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
load_config 函数无法访问给定目录之外的文件。它不需要知道该目录在磁盘上的位置。这让隔离测试变得非常简单:
use cap_std::fs::Dir;
use cap_tempfile::TempDir;
#[test]
fn test_load_config() -> std::io::Result<()> {
let tmp = TempDir::new(Default::default())?;
let dir = tmp.dir();
{
let mut f = dir.create("app.toml")?;
std::io::Write::write_all(&mut f, b"key = \"value\"")?;
}
let config = load_config(dir, "app.toml")?;
assert!(config.contains("value"));
Ok(())
}
cap-tempfile 将临时目录作为权能提供,因此即使你的测试也从不授予隐式权限。如果 load_config 尝试打开 ../etc/passwd 或任何绝对路径,无论测试或调用方提供什么,它都会失败。
切换时会有什么断裂
最大的限制是生态系统兼容性。大多数接触文件系统的 Rust crate 接受 &Path 或 PathBuf。它们期望隐式权限。如果你想将 cap-std 与例如一个从磁盘读取包含文件的配置解析 crate 一起使用,该 crate 需要支持 cap-std,或者你需要自己读取文件并将字符串内容传递给解析器。
这与异步 Rust 的迁移故事相同。就像接受 std::fs::File 的函数无法从期望异步文件句柄的异步代码中调用一样,接受 &Path 的函数也无法用 &Dir 调用。边界清晰,却是真实存在的。
还有一些 cap-std 故意不支持的操作。你无法创建逃逸目录树的硬链接。你无法跟随指向权能边界之外的任意符号链接。你无法调用 std::env::current_dir 并假设它在基于权能的程序中有任何意义。这些不是 bug。它们是安全模型的一部分。
性能通常不是问题。在带有 openat2 的 Linux 上,开销只是向现有系统调用添加几个额外的标志。在使用用户空间回退的平台上,路径解析更慢,但通常与实际 I/O 相比仍然可以忽略不计。
什么时候 cap-std 值得这份额外成本
你可能不需要为每个 CLI 工具或 Web 服务器都使用 cap-std。如果你信任你的依赖,并且你的威胁模型是外部攻击者而非恶意 crate,容器和正常的操作系统权限就足够了。
cap-std 在几个特定场景中大放异彩:
插件系统。 如果你的应用程序加载不受信任的 WASM module 或原生插件,cap-std 允许你向每个插件授予一个代表其沙箱的 Dir 权能。该插件无法在没有内核漏洞的情况下突破,因为 API 根本无法表达「打开任意文件」的概念。
构建工具和包管理器。 这些从互联网运行任意代码。一个使用 cap-std 的 build.rs 脚本无法读取你的 SSH 密钥,除非你明确地向它授予对你主目录的权能。
WASI 目标。 WebAssembly System Interface 建立在权能之上。cap-std 的 API 设计直接影响了 WASI,使用 cap-std 使得移植到 WASI 变得简单,因为安全模型已经对齐。
测试和可复现性。 默认情况下,接受 &Dir 而不是依赖当前工作目录的测试都是自包含的。你不需要 chdir 并恢复状态。你只需向测试传递一个临时目录权能。
入门
将 cap-std 添加到你的 Cargo.toml:
[dependencies]
cap-std = "3.0"
cap-tempfile = "3.0"
选择一个读取文件的 module,将其公共 API 改为接受 &Dir 而非 &Path,并更新调用方。你不必一次性迁移所有内容。cap-std 和 std::fs 可以在同一个二进制文件中共存。迁移是按 module 选择性加入的。
如果你维护一个读取文件的库,考虑在现有基于路径的 API 旁边添加一个基于 cap_std::fs::Dir 的 API。你的 WASI 用户会感谢你,注重安全的用户也能更轻松地对其应用进行沙箱化。
cap-std 仓库位于 github.com/bytecodealliance/cap-std。crate 文档包含平台支持说明和完整的 API 表面。从 Dir::open_ambient_dir 作为入口点开始,然后尝试移除每一个接受绝对路径的其他 std::fs 调用。你会惊讶于你的代码原来携带了多少隐式权限。