22 KiB
22 KiB
沙箱多平台测试方案(源码校准版)
更新日期:2026-04-09 审查范围:
SandboxInvoker.ts、policy.ts、@shared/types/sandbox.ts、windows-sandbox-helper/*.rs审查目标:准确性、完整性、一致性、深度、可操作性、安全性
1. 以源码为准的当前实现
1.1 模式与后端(事实)
- 沙箱模式:
strict | compat | permissive(默认compat)。 - 后端:
auto | docker | macos-seatbelt | linux-bwrap | windows-sandbox。 auto映射:darwin -> macos-seatbelt,linux -> linux-bwrap,win32 -> windows-sandbox。docker在SandboxInvoker.buildInvocation()中仅返回未包装命令并记录 warning(进程级未实现)。
1.2 macOS(seatbelt)
- 基线固定是
(deny default)。 - 非
permissive:- 允许
file-read*。 - 允许
process-exec的 regex:^/usr/bin/、^/bin/、^/usr/lib/。 - 允许 command 本身(literal)及其
realpath。 compat才会额外并入startupExecAllowlist。
- 允许
permissive:(allow file-write*)+(allow process-exec)+(allow signal)。- 非
permissive的写权限仅来自writablePaths(会同时尝试加入realpath)。 - 额外固定写白名单:
/dev/null、/dev/dtracehelper、/dev/urandom。
1.3 Linux(bwrap)
strict/compat都启用:--unshare-user-try --unshare-pid --unshare-uts --unshare-cgroup-trynetworkEnabled=false时加--unshare-net--tmpfs /tmp- 仅绑定
/dev/null、/dev/urandom、/dev/zero
strict:只读挂载最小面 + 命令相关目录:- 固定目录:
/usr /bin /sbin /lib /lib64 /etc /opt /usr/local - 额外加入
dirname(command),以及绝对参数对应目录(或目录本身)
- 固定目录:
compat:--ro-bind / /。permissive:--bind / / --dev-bind /dev /dev --proc /proc,且不做 namespace 隔离;networkEnabled在此模式下不生效。
1.4 Windows(qiming-sandbox-helper)
- 调用格式:
qiming-sandbox-helper run|serve --mode --cwd --policy-json -- <cmd>。 run默认write_restricted=true;permissive + run时会加--no-write-restricted。serve模式固定write_restricted=false(为允许孙进程)。workspace-write且有writablePaths:strict仅传第一个 root;compat/permissive传全部 roots。
- helper 内部策略关键点:
WorkspaceWrite才有writable_roots与network_access字段。ReadOnly固定has_full_network_access=false。- 网络“禁用”通过环境变量与 denybin stub(
ssh/scp)实现,不是内核级网络隔离。 compute_allow_paths()会在WorkspaceWrite下始终放行command_cwd、TEMP/TMP。.gitdeny 仅在被加入的 writable root 下且目录存在时生效。workspace-write下 ACE 默认持久化(persist_aces=true),不在进程退出时回滚。
1.5 回退策略(policy.ts)
enabled=false=> 直接type=none。- 后端不可用:
autoFallback=manual抛SANDBOX_UNAVAILABLE;- 否则统一降级
none,并app.emit("sandbox:unavailable", ...)。
- 当前实现里
startup-only与session没有行为差异(都走统一降级逻辑)。
2. 发现的问题清单(含修改建议)
2.1 critical
- Windows 网络隔离被文档描述为“全阻断”,与实现不符
- 问题:helper 仅做环境变量与命令桩劫持,无法强制阻断所有二进制直连网络。
- 位置:原文网络章节(Windows)。
- 建议修改:明确标注为“best-effort 网络抑制”,并新增绕过测试(自带 socket 客户端二进制)。
- Windows strict 写范围描述过度乐观,存在
cwd放行面
- 问题:helper 在
WorkspaceWrite下总是放行command_cwd;若上游未约束cwd,可扩大写面。 - 位置:原文“strict 仅第一个 writablePath 可写”。
- 建议修改:文档加入前置条件“调用方必须保证
cwd在工作区内”;新增负例测试。
2.2 major
autoFallback=session被描述为会话级差异,但源码无差异实现
- 问题:
startup-only与session当前都降级为none。 - 建议修改:文档改为“预留枚举,当前行为一致”;补充后续实现 TODO。
compat的 startup allowlist 价值被夸大
- 问题:当前两个调用点仅传入 command 本身,通常不增加额外可执行路径。
- 建议修改:文档改为“机制已支持,但当前调用链默认未传额外启动链”。
- Linux strict 挂载面文档遗漏
/usr/local
- 问题:源码已包含
/usr/local。 - 建议修改:更正目录清单,并统一所有章节。
- Linux permissive 网络行为描述前后冲突
- 问题:原文一处写“由 networkEnabled 控制”,另一处写“无隔离”;实现是不会
unshare-net。 - 建议修改:统一为“permissive 下不做 net namespace 隔离”。
- Windows read-only 与
network_access字段关系未说明
- 问题:
ReadOnly变体不消费network_access,实际固定无 full network。 - 建议修改:在文档中写清“read-only 下网络默认受限,不依赖 policy_json 的 network_access 字段”。
- Windows ACL 持久化副作用未纳入测试计划
- 问题:
workspace-write下 ACE 持久化可能造成长期 ACL 污染。 - 建议修改:新增“重复运行/卸载后 ACL 回收”测试与清理策略。
- 现有 Linux 集成测试样例不可直接执行风险高
- 问题:测试文件内
await exec(...)用法与 Node API 不匹配,计划中的“可直接执行”不成立。 - 建议修改:文档明确当前仅 macOS/Linux 部分集成用例存在,且需先修复测试脚本后纳入门禁。
2.3 minor
- 术语不一致(strict/compact 拼写混用)
- 建议:统一为
strict/compat/permissive。
- macOS 设备写白名单漏写
/dev/dtracehelper
- 建议:补全为
/dev/null、/dev/dtracehelper、/dev/urandom。
- 部分“绝对结论”缺少前置条件
- 建议:在表格增加“前置条件/调用方约束”列(如 cwd 约束、路径存在性)。
3. 需要补充的内容
- Windows 真实集成测试(当前仓库缺失)
run/serve两路径;strict/compat/permissive三模式;read-only/workspace-write两子模式。
- Windows 安全回归专项
cwd越界写入、TEMP/TMP放行面、.gitdeny 仅在存在时生效、world-writable 扫描上限(200 子目录 + 1 秒)。
- 回退可观测性测试
- 校验
app.emit("sandbox:unavailable")是否被 UI/日志消费;避免“以为开了沙箱,实际 none”。
- 模式一致性断言
startup-only与session当前等价,应有显式测试防止文档与实现再次漂移。
- 调用方约束测试
- ACP terminal/create 的
cwd是否被限制在工作区(当前未在AcpTerminalManager内做路径校验)。
4. 修订后的测试矩阵(可执行)
4.1 已有且可运行(先行)
- 单元测试:
src/main/services/sandbox/SandboxInvoker.test.tssrc/main/services/sandbox/sandboxProcessWrapper.test.tssrc/main/services/sandbox/policy.test.ts
- 集成测试(现有文件):
tests/sandbox-integration/macos-seatbelt.integration.test.tstests/sandbox-integration/linux-bwrap.integration.test.ts
执行命令:
cd crates/agent-electron-client
npm run test:run -- src/main/services/sandbox/SandboxInvoker.test.ts src/main/services/sandbox/sandboxProcessWrapper.test.ts src/main/services/sandbox/policy.test.ts
npm run test:run -- tests/sandbox-integration/macos-seatbelt.integration.test.ts
npm run test:run -- tests/sandbox-integration/linux-bwrap.integration.test.ts
4.2 必补(Windows)
| ID | 场景 | 预期 |
|---|---|---|
| WN-01 | workspace-write + strict,cwd 在 workspace 内写文件 |
成功 |
| WN-02 | workspace-write + strict,cwd 指向 workspace 外 |
必须失败(若成功则判定逃逸) |
| WN-03 | read-only 下写 workspace |
失败 |
| WN-04 | run + permissive(--no-write-restricted)创建子进程管道 |
成功 |
| WN-05 | serve 模式孙进程启动 |
成功 |
| WN-06 | network_access=false 下原生 socket 直连外网 |
应失败(若成功,标记为当前设计限制) |
| WN-07 | .git deny:root 存在 .git 时写入 .git/index.lock |
失败 |
| WN-08 | world-writable 目录审计超 200 子目录 | 仅扫描前 200,日志可见 |
| WN-09 | workspace-write 退出后 ACL 残留检查 |
残留可观测并有清理方案 |
5. 安全逃逸路径与当前状态
- Windows 网络旁路(critical)
- 状态:未彻底解决。
- 原因:仅 env/proxy/stub 抑制,无内核网络隔离。
- Windows
cwd扩写面(critical)
- 状态:依赖调用方约束。
- 原因:
compute_allow_paths()默认允许command_cwd。
- Windows ACL 持久化污染(major)
- 状态:已存在设计行为。
- 原因:
workspace-write设置persist_aces=true。
- Linux permissive 全盘可写(known risk)
- 状态:设计如此,仅用于排障。
- 控制:禁止作为默认策略,需审计日志。
- 回退到
none的误感知风险(major)
- 状态:已有
sandbox:unavailable事件,但需 UI 与监控闭环。
6. CI 门禁建议(修订)
PR required:三份单元测试必须通过。platform required:macOS + Linux 集成测试通过。release required:Windows 集成测试通过后再允许发布。security required:- 回退到
none必须有告警事件; - permissive 运行必须记录 warning;
- Windows 网络“best-effort”限制必须在报告中明确声明。
- 回退到
7. 里程碑(更新)
| 周次 | 目标 |
|---|---|
| 第1周 | 修正文档与现有测试脚本可执行性(尤其 Linux 集成脚本) |
| 第2周 | 补齐 Windows helper 集成测试(WN-01~WN-05) |
| 第3周 | 完成 Windows 安全回归(WN-06~WN-09) |
| 第4周 | 打通 CI 门禁与降级告警可视化 |
8. 第二轮复审:第一轮修正核验结果(逐项对源码)
结论:第一轮 12 条修正中,10 条“准确”,2 条“部分准确(需补充限定)”。
| ID | 第一轮修正 | 复审结论 | 源码依据 |
|---|---|---|---|
| R1 | Windows 网络并非“全阻断” | ✅ 准确 | windows-sandbox-helper/src/env.rs 的 apply_no_network_to_env() 仅设置代理/离线变量与 denybin(ssh/scp),非内核级隔离 |
| R2 | strict 写范围受 cwd 影响 |
✅ 准确 | acpTerminalManager.ts 将 cwd 加入 writablePaths;allow.rs 的 compute_allow_paths() 总是放行 command_cwd |
| R3 | autoFallback=session 与 startup-only 当前无行为差异 |
✅ 准确 | policy.ts 中 backend 不可用时,非 manual 统一返回 type=none,degraded=true |
| R4 | compat 启动链 allowlist 被高估 | ⚠️ 部分准确 | 机制有效;sandboxProcessWrapper.ts 当前仅传 [originalCommand],但未来可扩展,文档应写“当前调用链价值有限” |
| R5 | Linux strict 漏写 /usr/local |
✅ 准确 | SandboxInvoker.ts strict ro-bind 列表包含 /usr/local |
| R6 | Linux permissive 网络描述冲突 | ✅ 准确 | SandboxInvoker.ts permissive 分支不做 --unshare-net,networkEnabled 在此分支不生效 |
| R7 | Windows read-only 与 network_access 关系未说明 |
✅ 准确 | policy.rs ReadOnly => has_full_network_access=false,不消费 network_access 字段 |
| R8 | ACL 持久化副作用未纳入测试 | ✅ 准确 | main.rs persist_aces = is_workspace_write;cleanup 仅在 !persist_aces 回滚 ACE |
| R9 | Linux 集成测试可执行性描述偏乐观 | ✅ 准确 | linux-bwrap.integration.test.ts 存在 await exec("which bwrap") 等 callback API 误用 |
| R10 | strict/compact 术语混用 | ✅ 准确 | 当前实现统一为 strict/compat/permissive(@shared/types/sandbox.ts) |
| R11 | macOS 设备白名单漏写 /dev/dtracehelper |
✅ 准确 | SandboxInvoker.ts 固定写白名单含 /dev/dtracehelper |
| R12 | 缺少前置条件声明 | ⚠️ 部分准确 | 实现已隐含多项调用方前置约束(如 cwd、路径存在、MCP 进程 env),文档需显式化 |
9. 第二轮新增发现(第一轮遗漏)
9.1 环境变量链路存在“分层过滤不一致”
AcpTerminalManager(Windowsterminal/create路径)确实使用safeKeys最小白名单构造 env(可降低敏感变量泄露面)。- 但
windows-sandbox-helper的SandboxContext::setup()以std::env::vars()作为基底,env.rs内没有独立的safeKeys白名单逻辑。 sandboxed-bash-mcp.mjs执行 helper 时使用env = { ...process.env },仅删除ELECTRON_RUN_AS_NODE,因此若 MCP 进程继承到敏感变量,可能继续传入 helper/子命令。
结论:需要把“safeKeys 白名单”在文档中明确为“仅 terminal/create 路径有效,不是 helper 全局机制”,并新增泄露测试(见第 12 节)。
9.2 sandboxed-bash-mcp 强制 --no-write-restricted
- MCP 脚本构造 helper 参数时固定包含
run --no-write-restricted。 - 这意味着该路径始终依赖 DACL ACE 控制写权限,不使用 WRITE_RESTRICTED token 的二次约束。
结论:文档应把该行为从“可选/模式相关”改为“当前实现固定开启”。
9.3 world-writable 审计是“尽力而为”,且不会 fail-closed
audit.rs只扫描cwd直接子目录,最多 200 个,最长 1 秒。- 发现风险目录后仅尝试加 deny ACE;失败仅记日志,不阻断运行。
- 对工作区 root 下目录(
starts_with(workspace_roots))会跳过 deny。
结论:文档需明确这是补偿性审计,不是强制安全边界。
10. 深度审查补充(指定模块)
10.1 AcpTerminalManager(Windows terminal/create 路径)
- Windows + helper 可用时:
terminal/create通过SandboxInvoker.buildInvocation(...subcommand=run)走qiming-sandbox-helper run,parseJson=true。 writablePaths传参为[workspaceRoot, cwd](strict 下 helper policy 仅保留首个 root,但 helper 内仍会允许command_cwd)。- env 构建:
- 沙箱路径:仅取
safeKeys; - 非沙箱路径:透传宿主 env;
params.env直接覆盖(无二次过滤)。
- 沙箱路径:仅取
- 直接执行分支(macOS/Linux 或未启用 helper)在 Windows 下会
shell: true,helper 分支shell: false。
10.2 sandboxed-bash-mcp(Windows 专属 MCP 注入)
- 仅在
engine=claude-code且windows-sandbox启用时注入,且通过_meta禁用内置 Bash。 - MCP 工具名仍为
Bash,实际命令固定走 helper:run --no-write-restricted --mode <...> --cwd <process.cwd()> --policy-json <...> -- <bash/powershell> -c/-Command <user command>
- shell 优先级:
- 优先 Git Bash(支持 bash 语法);
- 否则回退 PowerShell。
- 该路径的 env 继承策略与
terminal/create不同(见 9.1)。
10.3 env.rs(环境变量处理)
- 当前职责是网络抑制、分页器设置、
/dev/null归一化,不负责敏感键白名单过滤。 apply_no_network_to_env()会注入代理与离线相关变量,并 prepend denybin(ssh/scpstub)。- 实际安全语义是“网络访问抑制 + 常见命令劫持”,不是“环境最小化”。
10.4 audit.rs(world-writable 扫描)
- 扫描范围:仅
cwd下一级目录,忽略 symlink/非目录。 - 资源上限:
MAX_CWD_CHILDREN=200,AUDIT_TIME_LIMIT_SECS=1。 - 判定:检查 DACL 中是否存在 world SID 的 write allow ACE。
- 处置:尝试对 capability SID 添加 deny write ACE;失败仅日志,流程继续。
10.5 acl.rs(DACL/ACE 细节)
add_allow_ace:为 capability SID 授予FILE_GENERIC_READ|WRITE|EXECUTE,继承到子对象。add_deny_write_ace:deny mask 包含FILE_GENERIC_WRITE与FILE_WRITE_*细项。revoke_ace:cleanup 阶段用REVOKE_ACCESS回收(仅persist_aces=false时执行)。allow_null_device:对\\.\NUL追加 allow ACE,避免 null 设备访问异常。
10.6 token.rs(Restricted Token)
- 基础标志:
DISABLE_MAX_PRIVILEGE | LUA_TOKEN。 write_restricted=true时额外加WRITE_RESTRICTED,并附加 3 个 restricting SIDs:- capability SID
- 当前 logon SID
- everyone SID
write_restricted=false(serve 或 run+--no-write-restricted)时不加 restricting SIDs,主要依赖 DACL。- 创建后仅重新启用
SeChangeNotifyPrivilege。
11. 与既有文档发现的纳入情况
11.1 sandbox/TEST-PLAN.md 纳入状态
| 项 | 状态 | 备注 |
|---|---|---|
| Gap-1~Gap-4(signal/exec/dev-bind/降级告警) | 已纳入 | 第 1/2/6 节已覆盖 |
| macOS/Linux 集成思路 | 部分纳入 | 已引用测试文件,但需补“当前脚本可执行性风险” |
| Windows 集成缺口 | 已纳入 | 第 4 节 WN-01~WN-09 |
| 并发/长稳/泄漏 | 未充分纳入 | 本轮第 12 节补全 |
11.2 sandbox/CODE-REVIEW.md 纳入状态
- 该文档多数问题属于
DockerSandbox/PermissionManager/WorkspaceManager通用模块,不全是“多平台 sandbox-plan”范围。 - 与本计划直接相关且应跟踪的项:
- 降级可观测性(已纳入)
- 测试覆盖不足(已纳入)
- 安全检测可绕过类问题(应在独立
PermissionManager安全计划跟进,不并入本计划的门禁结论)
结论:CODE-REVIEW 需在文档中标注“范围交集清单”,避免误解为“全部已在本计划闭环”。
12. 新增测试用例(第二轮补充)
12.1 环境变量泄露测试
| ID | 路径 | 步骤 | 预期 |
|---|---|---|---|
| ENV-01 | terminal/create |
预置宿主 ANTHROPIC_API_KEY=leak_test,在沙箱内执行 env |
输出中不应出现该键(除非通过 params.env 显式注入) |
| ENV-02 | sandboxed-bash-mcp |
同样预置敏感键,经 MCP Bash 执行 set/env |
若出现敏感键,标记高危并推动 MCP 路径加白名单过滤 |
| ENV-03 | helper run 直调 |
传入最小 env 与全量 env 对比 | 证明 helper 无内置 safeKeys 过滤(文档与实现一致) |
12.2 并发沙箱实例测试
| ID | 场景 | 预期 |
|---|---|---|
| CONC-01 | 50 个并发 terminal/create(上限) |
全部可创建并可回收 |
| CONC-02 | 第 51 个 terminal/create |
返回“Terminal limit reached”错误 |
| CONC-03 | 并发 run + serve 混合 |
无死锁;退出码与输出互不串扰 |
12.3 长时间运行稳定性
| ID | 场景 | 指标/阈值 |
|---|---|---|
| SOAK-01 | 单实例 serve 连续 24h |
无异常退出;内存增长斜率可控(例如 < 5%/h) |
| SOAK-02 | 周期性 run(每分钟一次,持续 12h) |
成功率 >= 99.9%,无句柄持续上涨 |
12.4 资源泄漏测试
| ID | 场景 | 预期 |
|---|---|---|
| LEAK-01 | 批量创建/释放 terminal 1000 次 | 无孤儿进程、无句柄持续累积 |
| LEAK-02 | workspace-write 退出后 ACL 检查 |
记录持久 ACE 残留;清理脚本可回收 |
| LEAK-03 | seatbelt profile 清理 | sandboxProcessWrapper cleanup 后临时 .sb 文件不存在 |
13. CI/CD 细化(GitHub Actions 可落地模板)
13.1 Workflow 建议
sandbox-unit.yml- 触发:
pull_request、push到主干 - 矩阵:
ubuntu-latest,macos-latest,windows-latest - 运行:
SandboxInvoker/policy/sandboxProcessWrapper单元测试
- 触发:
sandbox-integration-unix.yml- 触发:
pull_request - 矩阵:
ubuntu-latest,macos-latest - 运行:
tests/sandbox-integration/*(先修复 Linux 脚本 API 误用)
- 触发:
sandbox-integration-windows.yml- 触发:
workflow_dispatch+release/*分支必跑 - 运行:WN/ENV/CONC/SOAK/LEAK 中可自动化子集
- 触发:
sandbox-security-report.yml- 汇总各 job 产物,生成单一
sandbox-report.json+ Markdown 摘要并上传 artifact。
- 汇总各 job 产物,生成单一
13.2 报告格式与指标
- 建议统一 JSON schema:
platform,backend,mode,case_id,status,duration_ms,exit_code,notes
- 关键指标:
pass_rate(按平台/后端/模式分组)degrade_to_none_countpolicy_violation_count(如 strict 下越权写成功)env_leak_countresource_leak_signals(handle/process/tempfile/acl)
- PR 门禁阈值建议:
critical用例 100% 通过major用例 >= 99%env_leak_count == 0degrade_to_none_count必须附带告警记录与原因
14. 迁移与兼容性补充
14.1 旧版本策略迁移
- 已有兼容:
policy.ts支持windows.sandbox.mode -> windowsMode映射。 - 建议新增迁移规则:
- 缺失
autoFallback时默认补startup-only。 - 缺失
mode时默认补compat。 - 旧值非法时回退到
DEFAULT_SANDBOX_POLICY并记录一次迁移日志。
- 缺失
14.2 跨版本兼容要求
- 新增字段必须向后兼容(unknown 字段忽略,不影响旧客户端读取)。
- 禁止静默改变默认安全语义:
- 例如
windowsMode默认值变更必须伴随版本门禁与迁移提示。
- 例如
SandboxAutoFallback新枚举落地前,需先保证旧版本按“最安全可运行”路径降级(当前即none + degraded=true + 事件告警)。- helper 与主程序版本协商:
- 建议在 helper 启动时输出
version/capabilities,主程序按能力选择参数,避免跨版本参数不兼容。
- 建议在 helper 启动时输出
14.3 升级回滚策略
- 升级前备份
sandbox_policy(SQLite)。 - 新版本启动首轮执行策略 normalize + migrate,并写入
policy_migration_audit日志。 - 回滚时保留可识别字段,删除新版本专有字段(或降级到默认策略),确保旧版本不崩溃。