bordumb/capsec
GitHub: bordumb/capsec
为 Rust 提供编译时能力安全的三层防护框架,解决依赖代码实际行为不可见、权限边界不可控的问题。
Stars: 3 | Forks: 0
# capsec
[](https://github.com/bordumb/capsec/actions/workflows/ci.yml)
[](https://crates.io/crates/capsec)
[](https://docs.rs/capsec)
Rust 保证内存安全 —— capsec 保证行为安全。
### 快速开始
```
# 查看您的代码和依赖项的实际行为(零配置,零代码更改):
cargo install cargo-capsec
cargo capsec audit
```
`cargo audit` 检查 CVE。`cargo vet` 检查信任度。两者都无法告诉你代码实际上*做*了什么。没有什么能阻止你的 CSV 解析器打开一个 TCP socket 来回传遥测数据。
capsec 通过三层机制填补了这一空白:
1. **`cargo capsec audit`** —— 一个静态审计工具,扫描你的代码并报告每一个 I/O 调用。将其放入 CI 中,就能确切了解你的依赖项在做什么。
2. **编译时类型系统** —— 函数通过 `Has
` trait 边界声明其 I/O 权限,编译器会拒绝任何超出权限的代码。零运行时成本。
3. **运行时能力控制** —— `RuntimeCap`(可撤销)和 `TimedCap`(可过期)将静态能力与运行时有效性检查包装在一起,适用于服务器初始化或迁移窗口等动态场景。
审计工具负责发现问题。类型系统在编译时阻止问题。运行时 caps 处理权限需要动态变更的情况。
## cargo-capsec —— 静态能力审计
扫描 Rust 源代码中的环境权限(filesystem, network, env, process)并报告你的代码 —— 以及你的依赖项 —— 能对外部世界做什么。零配置,零代码修改。
### 安装
```
cargo install cargo-capsec
# 或从源码:
cargo install --path crates/cargo-capsec
```
### 运行
```
cargo capsec audit
```
```
my-app v0.1.0
─────────────
FS src/config.rs:8:5 fs::read_to_string load_config()
NET src/api.rs:15:9 TcpStream::connect fetch_data()
PROC src/deploy.rs:42:17 Command::new run_migration()
Summary
───────
Crates with findings: 1
Total findings: 3
Categories: FS: 1 NET: 1 ENV: 0 PROC: 1
1 critical-risk findings
```
### 添加到 CI
```
# .github/workflows/capsec.yml
name: Capability Audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-capsec
- run: cargo capsec audit --fail-on high --quiet
```
PR 中出现了新的高风险 I/O?CI 失败。没有新的 I/O?CI 通过。团队可以使用 `--baseline` 和 `--diff` 逐步采用,仅标记*新发现*的问题。
要查看实际效果,可以参考以下内容:
* [CI/CD](https://github.com/bordumb/capsec/blob/main/.github/workflows/ci.yml#L57)
* [Pre-Commit Hook](https://github.com/bordumb/capsec/blob/main/.pre-commit-config.yaml#L32)
## capsec —— 编译时能力强制执行
审计工具告诉你代码做了什么。类型系统控制代码*被允许*做什么。
### 使用 capsec 之前
任何函数都可以做任何事。函数签名具有误导性 —— 它看起来是纯函数,但它读取文件并打开 socket:
```
// This function signature says nothing about I/O.
// But inside, it reads from disk and phones home over the network.
pub fn process_csv(input: &[u8]) -> Vec> {
let config = std::fs::read_to_string("/etc/app/config.toml")
.unwrap_or_default();
if let Ok(mut stream) = std::net::TcpStream::connect("telemetry.example.com:8080") {
stream.write_all(input).ok();
}
parse(input, &config)
}
```
Rust 编译器对此完全满意。Clippy 不会标记它。没有什么能阻止它。
### 使用 capsec 之后
函数在类型签名中声明其 I/O 需求。编译器强制执行这些需求:
```
use capsec::prelude::*;
// Define a context with exactly the permissions your app needs.
// The macro generates Cap fields, constructor, and Has impls.
#[capsec::context]
struct AppCtx {
fs: FsRead,
net: NetConnect,
}
// Leaf functions take &impl Has
— works with raw caps AND context structs.
pub fn load_config(path: &str, cap: &impl Has) -> Result {
capsec::fs::read_to_string(path, cap)
}
// Intermediate functions take a single context reference — not N separate caps.
pub fn app_logic(ctx: &AppCtx) -> Result {
load_config("/etc/app/config.toml", ctx) // ctx satisfies Has
}
// #[capsec::main] injects the capability root automatically.
#[capsec::main]
fn main(root: CapRoot) {
let ctx = AppCtx::new(&root);
let config = app_logic(&ctx).unwrap();
}
```
每个能力都可追溯至 `CapRoot`。如果函数在没有被授予 `Cap` 的情况下使用 capsec 包装器(如 `capsec::fs::read_to_string`),代码将无法编译。审计工具会捕获完全绕过 capsec 包装器的代码 —— 直接调用 `std::fs`、使用 FFI 或将 I/O 隐藏在重导出之后。
### 编译器实际上会说什么
**错误的能力类型** —— 在需要 `Cap` 的地方传递了 `Cap`:
```
let net_cap = root.grant::();
let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
```
```
error[E0277]: the trait bound `Cap: Has` is not satisfied
--> src/main.rs:4:55
|
4 | let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
| -------------------------- ^^^^^^^^ the trait `Has` is not implemented for `Cap`
| |
| required by a bound introduced by this call
```
**缺少能力** —— 调用 capsec 函数时根本没有提供 token:
```
let _ = capsec::fs::read_to_string("/etc/passwd");
```
```
error[E0061]: this function takes 2 arguments but 1 argument was supplied
--> src/main.rs:2:13
|
2 | let _ = capsec::fs::read_to_string("/etc/passwd");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^--------------- argument #2 of type `&_` is missing
|
help: provide the argument
|
2 | let _ = capsec::fs::read_to_string("/etc/passwd", /* cap */);
| +++++++++++
```
**跨类别违规** —— `FsAll` 包含 `FsRead` 和 `FsWrite`,但不包含 `NetConnect`:
```
let fs_all = root.grant::();
needs_net(&fs_all); // fn needs_net(_: &impl Has) {}
```
```
error[E0277]: the trait bound `Cap: Has` is not satisfied
--> src/main.rs:3:15
|
3 | needs_net(&fs_all);
| --------- ^^^^^^^ the trait `Has` is not implemented for `Cap`
| |
| required by a bound introduced by this call
```
这些是真实的 `rustc` 错误 —— 没有自定义错误框架,没有运行时 panic。Rust 编译器负责执行。
### 这为你带来了什么
| | 之前 | 之后 |
|--|--------|-------|
| 任何函数都能读文件吗? | 是 | 只有拥有 `Cap` 的函数才能 |
| 任何函数都能打开 socket 吗? | 是 | 只有拥有 `Cap` 的函数才能 |
| 你能审计谁拥有什么权限吗? | 只能靠 Grep 碰运气 | Grep `Has` 即可 |
| 运行时成本? | 不适用 | 零 —— 所有类型在编译时被擦除 |
### 安全模型
capsec 针对的是**协作式安全 Rust** —— 使用 capsec 包装器的代码无法超出其声明的权限,编译器以零运行时成本强制执行这一点。
`Has` trait 是开放实现的 —— 自定义上下文结构体可以实现它以委托能力访问。安全性得以维持是因为 `Cap::new()` 是 `pub(crate)` 的:外部代码无法在 safe Rust 中伪造 `Cap
`。`Permission` trait 保持封印 —— 外部 crate 无法发明新的权限类型。
capsec **不**防范的内容:
- **`unsafe` 代码**,通过 `transmute`、`MaybeUninit` 或指针技巧伪造能力 token。类型系统仅在 safe Rust 内部是健全的。(`capsec-tests/tests/type_system.rs` 中的对抗性测试套件记录了这些攻击,并确认它们需要 `unsafe`。)
- **直接 `std` 调用**,绕过 capsec 包装器。函数总是可以在没有能力 token 的情况下调用 `std::fs::read()` —— 编译器不会阻止它。这就是 `cargo capsec audit` 的用武之地:它静态检测这些调用。
- **FFI 和内联汇编**,直接与 OS 交互。审计工具会标记 `extern` 块,但无法推断外部代码的行为。
这三层是互补的:**类型系统**在选择了加入的代码内强制执行边界,**审计工具**发现尚未选择加入的代码,**运行时 caps** 处理动态权限生命周期。单独一层都不完整 —— 它们一起提供了纵深防御。
capsec 附带一个包含 74 个测试的对抗性安全套件,记录了每个已知的规避向量和攻击面 —— unsafe 伪造、`std` 绕过、FFI 逃生舱口、上下文委托攻击等。大多数安全工具不会编目其自身的弱点。capsec 做到了,所以你确切知道什么被覆盖了,什么没有。参见 [`capsec-tests/tests/type_system.rs`](crates/capsec-tests/tests/type_system.rs) 和 [`capsec-tests/tests/audit_evasion.rs`](crates/capsec-tests/tests/audit_evasion.rs)。
## 运行时能力控制
静态 `Cap
` token 是永久的 —— 一旦授予,它们永远有效。对于权限应该是临时或可撤销的场景,capsec 提供了运行时能力包装器。
### 可撤销能力
为服务器启动授予网络访问权限,然后撤销它,以便在运行时无法建立新连接:
```
use capsec::prelude::*;
#[capsec::main]
fn main(root: CapRoot) -> Result<(), Box> {
// Wrap a capability with a revocation handle
let (runtime_cap, revoker) = RuntimeCap::new(root.net_connect());
// During startup: try_cap() returns a real Cap
let cap = runtime_cap.try_cap()?;
init_connection_pool(&cap);
// After init: revoke — no new connections possible
revoker.revoke();
// Now try_cap() returns Err(CapSecError::Revoked)
assert!(runtime_cap.try_cap().is_err());
Ok(())
}
```
运行时 caps 与上下文模式组合 —— 包装单个能力以进行撤销,同时保持上下文的人体工程学:
```
#[capsec::context]
struct ServerCtx {
fs: FsRead,
net: NetConnect,
}
// Grant static fs access + revocable net access
let ctx = ServerCtx::new(&root); // static caps for the context
let (net_rt, revoker) = RuntimeCap::new(root.net_connect()); // revocable net cap
// Use ctx for Has bounds, net_rt.try_cap()? for revocable net access
```
### 有时间限制的能力
为迁移窗口授予临时写入权限:
```
use capsec::prelude::*;
use std::time::Duration;
#[capsec::main]
fn main(root: CapRoot) -> Result<(), Box> {
let timed_cap = TimedCap::new(root.fs_write(), Duration::from_secs(30));
// Within the window: try_cap() succeeds
let cap = timed_cap.try_cap()?;
capsec::fs::write("/tmp/migration.txt", "data", &cap)?;
// After TTL: try_cap() returns Err(CapSecError::Expired)
// timed_cap.remaining() returns Duration::ZERO
Ok(())
}
```
### 关键属性
- `RuntimeCap` 和 `TimedCap` **不**实现 `Has` —— 在每个调用点通过 `try_cap()` 显式表达可失败性
- 两者默认都是 `!Send` 的 —— 使用 `make_send()` 以选择加入跨线程传输
- 克隆 `RuntimeCap` 会共享撤销标志 —— 撤销一个即撤销所有克隆
- `Revoker` 是 `Send + Sync + Clone` 的 —— 可以从任何线程撤销
### capsec 与其他工具的对比
| 工具 | 方法 | 层级 |
|------|----------|-------|
| **capsec** | 编译时类型(`Has
` 边界)+ 运行时 caps(`RuntimeCap`,`TimedCap`)+ 静态审计 | 源码级,协作式 |
| **[cap-std](https://github.com/bytecodealliance/cap-std)** | 运行时能力句柄(移除环境权限) | OS 级,面向 WASI |
| **[cargo-scan](https://github.com/AlfredoSystems/cargo-scan)** | 危险 API 使用的静态分析 | 源码级,研究原型 |
| **[cargo-cgsec](https://github.com/nicholasgasior/cargo-cgsec)** | 调用图能力分析(Capslock 移植) | 源码级,仅审计 |
`cap-std` 在不同的层级运作 —— 它在运行时将 OS 级文件描述符替换为能力句柄,目标是 WASI 沙箱。capsec 在类型级别工作,零运行时成本,不需要 OS 支持。两者是互补的:你可以在 capsec 把关的函数内使用 `cap-std` 句柄。
`cargo-scan`(来自 UC San Diego)执行与 `cargo capsec audit` 类似的静态分析。capsec 增加了类型系统强制层,并作为包含这两个工具的单个工作区发布。
`cargo-cgsec` 是 Google Capslock 的 Rust 移植版,由 Rust Foundation 资助。它执行调用图分析以识别能力使用 —— 仅审计,无强制或运行时层。capsec 通过 `cargo capsec audit` 覆盖相同的审计面,并增加了编译时类型强制和运行时能力控制。
## 许可证
Apache-2.0
标签:Capability-based Security, Cargo 插件, FS/NET 权限, I/O 监控, JSONLines, Runtime Control, Rust, Security Audit, Streamlit, 云安全监控, 依赖审计, 内存安全, 可视化界面, 文档安全, 最小权限原则, 权限控制, 类型系统, 编译时安全, 网络信息收集, 网络流量审计, 行为安全, 访问控制, 通知系统, 防御 telemetry, 零信任, 静态分析