Files
qiming/qimingclaw/docs/sandbox/TEST-PLAN.md

181 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sandbox 测试验证方案
> 创建时间2026/04/08
> 状态:已计划,待实施
## Context
目前三平台沙箱macOS seatbelt / Linux bwrap / Windows helper没有任何测试文件覆盖无法验证危险操作是否被正确拦截。需要新增
- 单元测试(无需真实沙箱二进制)
- 跨平台集成测试
- 并修复已发现的 4 个安全 Gap
## 关键文件
| 文件 | 作用 |
|------|------|
| `src/main/services/sandbox/SandboxInvoker.ts` | seatbelt profile / bwrap args 生成 |
| `src/main/services/sandbox/sandboxProcessWrapper.ts` | ACP 进程包装 |
| `src/main/services/sandbox/policy.ts` | resolveSandboxType fallback |
| `vitest.config.ts` | 测试配置(已包含 `tests/**` |
---
## 实现计划
### 1. 单元测试(`src/main/services/sandbox/`
#### SandboxInvoker.test.ts
- mock `fs/promises.writeFile`,捕获 profile 内容
- **macOS**
- profile 以 `(version 1)` `(deny default)` 开头
- `(allow network*)` 当 networkEnabled=true
- seatbeltProfilePath 返回值非 undefined供调用方清理
- **Linux**
- `--ro-bind / /` 存在于 bwrap args
- `--tmpfs /tmp` 存在
- `--dev-bind /dev /dev` 存在(记录 rw gap
- networkEnabled=false → `--unshare-net` 存在
- networkEnabled=true → `--unshare-net` 不存在
- 每个 writablePath 生成 `--bind <p> <p>`,精确索引
- **Windows**
- helper 不存在时抛 `SANDBOX_UNAVAILABLE` reject
- networkEnabled=false 时 policy JSON network_access=falseJSON 解析验证)
- writable_roots 含 workspaceJSON 解析验证)
- **none**:原样返回 command/args
- **docker**:返回原始命令并 log.warn
#### sandboxProcessWrapper.test.ts
- 测试 `buildSandboxedSpawnArgs()` 对各平台的包装结果
- enabled=false → 原样返回
- docker backend → 透传 + warn
#### policy.test.ts
- mock `fs.existsSync` 全返回 false + mock SQLite
- enabled=false → type=none, degraded=false
- 后端不可用 → degraded=true, reason 非空
- fallback 结果永远是 none不会是其他可用类型
### 2. 集成测试(`tests/sandbox-integration/`
#### shared-integration-utils.ts
```typescript
runInSeatbelt(profileContent: string, shellCommand: string): Promise<{exitCode, stdout, stderr, timedOut}>
runInBwrap(bwrapArgs: string[]): Promise<{exitCode, stdout, stderr, timedOut}>
expectBlocked(result): void
expectAllowed(result): void
```
#### macos-seatbelt.integration.test.ts
```typescript
describe.skipIf(process.platform !== 'darwin' || !existsSync('/usr/bin/sandbox-exec'))
```
使用与生产完全一致的 profile 格式(`(deny default)` + explicit allowsworkspace writableno network
| 类别 | 命令 | 期望 |
|------|------|------|
| 文件写入 | `echo ... >> /etc/hosts` | BLOCKED |
| 文件写入 | `echo ... >> /etc/passwd` | BLOCKED |
| 文件写入 | `touch /usr/bin/evil-binary` | BLOCKED |
| 文件写入 | `touch ~/Documents/exfil.txt` | BLOCKED |
| 文件写入 | `touch /tmp/attacker-file.txt`/tmp 不在 writablePaths| BLOCKED |
| 文件写入 | workspace 内写入 | ALLOWED |
| 文件删除 | `rm /etc/resolv.conf` | BLOCKED |
| 文件删除 | `rm /usr/bin/ls` | BLOCKED |
| 提权 | `sudo id` | BLOCKED |
| 系统命令 | `/sbin/shutdown -h now`exec 允许但 root 权限不足)| 输出 Permission 字样 |
| 网络 | `curl http://example.com`(无 allow network*| BLOCKED |
`(allow network*)` 的 profile 下 curl 应 ALLOWED。
#### linux-bwrap.integration.test.ts
```typescript
describe.skipIf(process.platform !== 'linux')
```
测试场景(含 `--unshare-net``--tmpfs /tmp`、workspace bind
| 类别 | 命令 | 期望 |
|------|------|------|
| 文件写入 | `/etc/hosts`, `/etc/passwd`, `/usr/bin/evil`, `/home/exfil.txt` | BLOCKED |
| 文件写入 | workspace 内 | ALLOWED |
| 文件写入 | `/tmp/sandbox-tmp.txt`tmpfs | ALLOWED但宿主 /tmp 无此文件 |
| 文件删除 | `/etc/hostname`, `/bin/sh` | BLOCKED |
| 网络 | curl--unshare-net | BLOCKED |
| 网络 | loopback ping--unshare-net 下仍有 lo| ALLOWEDlocalhost 可用)|
| 提权 | `sudo id` | BLOCKED |
| 系统命令 | `reboot`, `halt` | BLOCKED |
| /dev gap | `mknod /dev/test-device` | BLOCKED |
| /dev gap | `echo evil > /dev/sda` | BLOCKED |
| PID 命名空间 | `ps aux \| wc -l` | < 10 行(与宿主隔离)|
---
### 3. 修复 4 个安全 Gap
#### Gap 1`allow signal` 过于宽泛macOS
- **位置**`SandboxInvoker.ts` `buildSeatbeltProfile()`
- **风险**:允许向任意可 reach 进程发信号UNIX 权限仍阻止 PID 1但若以 root 运行则有风险)
- **修复**:改为 `(allow signal (target self))`
#### Gap 2`allow process-exec` 无限制macOS
- **位置**`SandboxInvoker.ts` `buildSeatbeltProfile()`
- **风险**:可 exec `/sbin/shutdown``/sbin/reboot` 等系统命令,依赖权限不足阻止而非沙箱
- **修复**:用路径白名单限制可 exec 的目录
#### Gap 3`--dev-bind /dev /dev` 可读写Linux
- **位置**`SandboxInvoker.ts` `buildBwrap()`
- **风险**:若 `--unshare-user-try` fallback 失败(内核不支持),/dev 可写
- **修复**:改为最小 device allowlist`--dev-bind /dev/null``/dev/urandom``/dev/zero``/dev/random`
#### Gap 4降级到 none 无用户提示
- **位置**`policy.ts` `resolveSandboxType()`
- **风险**:用户以为有沙箱保护,实际在无沙箱运行
- **修复**:降级时 emit `sandbox:unavailable` 事件,日志级别从 debug 改为 warn
---
## 文件清单(新增)
```
src/main/services/sandbox/
SandboxInvoker.test.ts # 新增
sandboxProcessWrapper.test.ts # 新增
policy.test.ts # 新增
tests/sandbox-integration/
shared-integration-utils.ts # 新增
macos-seatbelt.integration.test.ts # 新增
linux-bwrap.integration.test.ts # 新增
```
## 运行方式
```bash
cd crates/agent-electron-client
# 单元测试(全平台)
npm run test:run -- src/main/services/sandbox/
# macOS 集成(在 macOS 机器上)
npm run test:run -- tests/sandbox-integration/macos-seatbelt.integration.test.ts
# Linux 集成(在 Linux 机器上)
npm run test:run -- tests/sandbox-integration/linux-bwrap.integration.test.ts
```
## CI 建议
`package.json` 增加集成测试的 platform-specific 配置,确保 CI 在对应平台运行。