chore: initialize qiming workspace repository
This commit is contained in:
180
qimingclaw/docs/sandbox/TEST-PLAN.md
Normal file
180
qimingclaw/docs/sandbox/TEST-PLAN.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# 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=false(JSON 解析验证)
|
||||
- writable_roots 含 workspace(JSON 解析验证)
|
||||
- **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 allows,workspace writable,no 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)| ALLOWED(localhost 可用)|
|
||||
| 提权 | `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 在对应平台运行。
|
||||
Reference in New Issue
Block a user