chore: initialize qiming workspace repository
This commit is contained in:
556
qimingclaw/docs/sandbox/CODE-REVIEW.md
Normal file
556
qimingclaw/docs/sandbox/CODE-REVIEW.md
Normal file
@@ -0,0 +1,556 @@
|
||||
# 沙箱实现代码审查报告
|
||||
|
||||
**审查日期**: 2026-03-22
|
||||
**审查者**: Claude Code
|
||||
**版本**: 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体评价
|
||||
|
||||
### 评分: 7.5 / 10
|
||||
|
||||
### 优点
|
||||
- 类型定义完整,覆盖了沙箱、工作区、权限、执行结果等核心概念
|
||||
- 错误处理体系完善,有专门的错误类和错误码
|
||||
- 抽象基类设计合理,支持多种沙箱类型(Docker、WSL、Firejail)
|
||||
- 权限管理实现了分层策略(自动批准、需要确认、禁止)
|
||||
- 事件驱动架构,支持状态监听
|
||||
|
||||
### 不足
|
||||
- 部分安全检查存在漏洞
|
||||
- 缺少持久化存储实现
|
||||
- 代码注释不完整
|
||||
- 部分边界情况未处理
|
||||
- 测试覆盖未知
|
||||
|
||||
---
|
||||
|
||||
## 2. 各文件详细评价
|
||||
|
||||
### 2.1 `src/shared/types/sandbox.ts` - 类型定义
|
||||
|
||||
**评分**: 8.5/10
|
||||
|
||||
#### 优点
|
||||
- 类型定义非常完整,覆盖了所有核心概念
|
||||
- JSDoc 注释详细
|
||||
- 支持 Harness 架构的 CP 工作流概念(Checkpoint、GateStatus)
|
||||
- 事件类型定义完整(SandboxEvents 接口)
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🟡 中 | `PermissionLevel` 类型定义了 0-3 级别,但代码中未使用 | L34 |
|
||||
| 🟡 中 | `WorkflowState.workspaces` 使用 `Record<string, WorkspaceRecord>` 但 `Workspace` 对象更完整 | L367 |
|
||||
| 🟢 低 | `FileInfo.permissions` 是可选的字符串,缺少类型定义 | L213 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 添加 PermissionLevel 的使用场景说明或移除
|
||||
// 建议: FileInfo.permissions 使用更结构化的类型
|
||||
export interface FilePermissions {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
execute: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `src/shared/errors/sandbox.ts` - 错误类
|
||||
|
||||
**评分**: 8/10
|
||||
|
||||
#### 优点
|
||||
- 错误码枚举完整,覆盖所有场景
|
||||
- 错误类层次结构清晰(基类 SandboxError + 特化类)
|
||||
- `getUserMessage()` 提供用户友好的错误信息
|
||||
- `isRecoverable()` 和 `requiresUserIntervention()` 有助于错误处理决策
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🔴 高 | `toSandboxError` 可能丢失原始错误的堆栈信息 | L407-417 |
|
||||
| 🟡 中 | `cause` 属性在 `toJSON` 中只序列化 message,可能丢失重要信息 | L146 |
|
||||
| 🟡 中 | `PermissionError` 硬编码了 `PERMISSION_DENIED`,无法使用其他权限相关错误码 | L313-316 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 保留完整的错误链
|
||||
if (error instanceof Error) {
|
||||
const sandboxError = new SandboxError(error.message || defaultMessage, defaultCode, {
|
||||
...options,
|
||||
cause: error,
|
||||
});
|
||||
// 保留原始堆栈
|
||||
sandboxError.stack = error.stack;
|
||||
return sandboxError;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `src/main/services/sandbox/SandboxManager.ts` - 基类
|
||||
|
||||
**评分**: 7.5/10
|
||||
|
||||
#### 优点
|
||||
- 抽象方法定义清晰,子类必须实现
|
||||
- 提供了实用的静态工具方法(`parseMemoryLimit`, `formatMemory`)
|
||||
- 工作区验证逻辑完善
|
||||
- 支持事件发射
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🔴 高 | `normalizePath` 使用 `toLowerCase()` 可能导致路径比较问题(不区分大小写的文件系统) | L240-241 |
|
||||
| 🟡 中 | `destroy()` 方法捕获错误后使用 `console.error` 而非 `log` | L281 |
|
||||
| 🟡 中 | `generateWorkspaceId` 仅使用时间戳,高并发下可能冲突 | L247-249 |
|
||||
| 🟢 低 | `emitEvent` 的泛型参数 `T extends string` 未提供实际类型约束 | L266 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 使用 UUID 或更可靠的 ID 生成方式
|
||||
protected generateWorkspaceId(sessionId: string): string {
|
||||
const { randomUUID } = require('crypto');
|
||||
return `workspace-${sessionId}-${randomUUID().slice(0, 8)}`;
|
||||
}
|
||||
|
||||
// 建议: 路径规范化不应使用 toLowerCase
|
||||
protected normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 `src/main/services/sandbox/DockerSandbox.ts` - Docker 实现
|
||||
|
||||
**评分**: 7/10
|
||||
|
||||
#### 优点
|
||||
- 完整的 Docker 生命周期管理
|
||||
- 资源限制配置(内存、CPU)
|
||||
- 失败时自动清理资源
|
||||
- 超时处理完善
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🔴 高 | **命令注入漏洞**: `docker image inspect ${image}` 未对镜像名进行转义 | L142 |
|
||||
| 🔴 高 | **命令注入漏洞**: `docker pull ${image}` 同样存在问题 | L147 |
|
||||
| 🔴 高 | `startContainer` 中 `args.join(" ")` 构建命令字符串,参数未转义 | L672 |
|
||||
| 🟡 中 | `execAsync` 超时可能导致僵尸进程 | 多处 |
|
||||
| 🟡 中 | `readFile`/`writeFile` 直接操作主机文件系统,未通过容器 | L361-408 |
|
||||
| 🟡 中 | `getDirectorySize` 递归计算大目录可能很慢 | L794-813 |
|
||||
| 🟢 低 | `tail -f /dev/null` 作为容器保持运行的方式不够优雅 | L667-669 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 使用 spawn 并正确传递参数,避免命令注入
|
||||
private async ensureImageExists(): Promise<void> {
|
||||
const image = this.dockerConfig.dockerImage;
|
||||
|
||||
// 验证镜像名格式
|
||||
if (!/^[a-zA-Z0-9._/:-]+$/.test(image)) {
|
||||
throw new SandboxError("无效的镜像名称", SandboxErrorCode.CONFIG_INVALID);
|
||||
}
|
||||
|
||||
try {
|
||||
await execAsync(`docker image inspect`, [image], { timeout: 10000 });
|
||||
} catch {
|
||||
await execAsync(`docker pull`, [image], { timeout: 300000 });
|
||||
}
|
||||
}
|
||||
|
||||
// 建议: 使用 spawn 替代 exec 构建命令
|
||||
private async startContainer(...): Promise<string> {
|
||||
const args = ["run", "-d", "--name", containerName, ...];
|
||||
const proc = spawn("docker", args);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 `src/main/services/sandbox/PermissionManager.ts` - 权限管理
|
||||
|
||||
**评分**: 7/10
|
||||
|
||||
#### 优点
|
||||
- 分层权限策略(自动批准、需要确认、禁止)
|
||||
- 安全命令白名单
|
||||
- 危险命令黑名单检测
|
||||
- 权限缓存机制
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🔴 高 | `checkDangerousOperation` 使用 `includes` 检测,可被绕过(如 `sudo\n` 或 `sud o`) | L628-638 |
|
||||
| 🔴 高 | 危险命令检测对大小写敏感,`SUDO` 或 `SuDo` 可绕过 | L628-638 |
|
||||
| 🟡 中 | `requestPermission` 超时后 pendingRequests 中的请求未清理(虽然 delete 了,但监听器还在) | L305-329 |
|
||||
| 🟡 中 | 敏感路径检测逻辑错误: `!lowerTarget.startsWith("/etc/")` 永远为 true | L651 |
|
||||
| 🟡 中 | 缓存键包含完整 target,大路径可能导致内存问题 | L606-611 |
|
||||
| 🟢 低 | 60 秒超时硬编码 | L313 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 增强危险命令检测
|
||||
private checkDangerousOperation(
|
||||
type: PermissionType,
|
||||
target: string,
|
||||
): { isDangerous: boolean; reason?: string } {
|
||||
// 规范化: 移除空白字符,转小写
|
||||
const normalizedTarget = target.toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
|
||||
for (const dangerous of DANGEROUS_COMMANDS) {
|
||||
// 使用更严格的匹配
|
||||
const pattern = new RegExp(`\\b${escapeRegex(dangerous.toLowerCase())}\\b`);
|
||||
if (pattern.test(normalizedTarget)) {
|
||||
return {
|
||||
isDangerous: true,
|
||||
reason: `检测到危险操作: ${dangerous}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 修复敏感路径检测
|
||||
if (this.isPathPermission(type)) {
|
||||
if (normalizedTarget.includes('/.ssh') || normalizedTarget.includes('\\.ssh')) {
|
||||
return { isDangerous: true, reason: "禁止访问 SSH 目录" };
|
||||
}
|
||||
if (normalizedTarget.startsWith('/etc/')) {
|
||||
return { isDangerous: true, reason: "禁止访问系统配置目录" };
|
||||
}
|
||||
}
|
||||
|
||||
return { isDangerous: false };
|
||||
}
|
||||
|
||||
// 建议: 修复超时后的清理
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(request.id);
|
||||
this.removeAllListeners(`permission:${request.id}`); // 清理监听器
|
||||
reject(...);
|
||||
}, 60000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.6 `src/main/services/sandbox/WorkspaceManager.ts` - 工作区管理
|
||||
|
||||
**评分**: 8/10
|
||||
|
||||
#### 优点
|
||||
- 整合 SandboxManager 和 PermissionManager
|
||||
- 统一的工作区生命周期管理
|
||||
- 权限检查与文件操作集成
|
||||
- 保留策略支持
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🟡 中 | `cleanupExpired` 中工作区数量检查逻辑可能导致多次清理同一工作区 | L399-410 |
|
||||
| 🟡 中 | `validatePathInWorkspace` 重复实现了基类的方法 | L536-550 |
|
||||
| 🟡 中 | `destroy` 方法的 `force` 参数在错误时只记录日志,不抛出异常,可能隐藏问题 | L167-181 |
|
||||
| 🟢 低 | `isPathPermission` 重复定义(PermissionManager 中已有) | L529-531 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 优化工作区数量检查逻辑
|
||||
if (policy.maxWorkspaces && workspaces.length > policy.maxWorkspaces) {
|
||||
const sorted = [...workspaces].sort(
|
||||
(a, b) => a.lastAccessedAt.getTime() - b.lastAccessedAt.getTime(),
|
||||
);
|
||||
const toRemove = sorted.slice(0, workspaces.length - policy.maxWorkspaces);
|
||||
const idsToRemove = new Set(toRemove.map(w => w.id));
|
||||
|
||||
if (idsToRemove.has(workspace.id)) {
|
||||
shouldCleanup = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 建议: 复用基类的路径验证方法
|
||||
private validatePathInWorkspace(workspace: Workspace, path: string): void {
|
||||
// 直接调用 SandboxManager 的方法
|
||||
// 或提取为共享工具函数
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.7 `src/main/ipc/sandboxHandlers.ts` - IPC 通道
|
||||
|
||||
**评分**: 8/10
|
||||
|
||||
#### 优点
|
||||
- 服务注入模式,解耦良好
|
||||
- 统一的错误处理
|
||||
- 完整的 JSDoc 注释
|
||||
- IPC 通道命名规范
|
||||
|
||||
#### 问题
|
||||
|
||||
| 严重程度 | 问题描述 | 位置 |
|
||||
|---------|---------|------|
|
||||
| 🟡 中 | 服务类型定义使用 `typeof sandboxService`,但初始值为 `null`,类型推断可能不准确 | L31-50 |
|
||||
| 🟡 中 | `handleError` 返回的 `code` 字段类型不明确(`code?: string`) | L118 |
|
||||
| 🟡 中 | 缺少 IPC 通道注销函数 | - |
|
||||
| 🟢 低 | 日志记录了敏感信息(如完整路径) | L264 等 |
|
||||
|
||||
#### 建议
|
||||
```typescript
|
||||
// 建议: 使用接口定义服务类型
|
||||
interface SandboxService {
|
||||
createWorkspace(sessionId: string, options?: CreateWorkspaceOptions): Promise<Workspace>;
|
||||
destroyWorkspace(sessionId: string): Promise<void>;
|
||||
// ...
|
||||
}
|
||||
|
||||
let sandboxService: SandboxService | null = null;
|
||||
|
||||
// 建议: 添加注销函数
|
||||
export function unregisterSandboxHandlers(): void {
|
||||
ipcMain.removeHandler("sandbox:create");
|
||||
ipcMain.removeHandler("sandbox:destroy");
|
||||
// ...
|
||||
}
|
||||
|
||||
// 建议: 错误码使用枚举类型
|
||||
function handleError(error: unknown, operation: string): {
|
||||
success: false;
|
||||
error: string;
|
||||
code?: SandboxErrorCode;
|
||||
} {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 发现的问题汇总
|
||||
|
||||
### 3.1 安全漏洞 (🔴 高优先级)
|
||||
|
||||
| # | 问题 | 文件 | 风险 |
|
||||
|---|------|------|------|
|
||||
| 1 | Docker 命令注入漏洞 | DockerSandbox.ts | 攻击者可通过构造镜像名执行任意命令 |
|
||||
| 2 | 危险命令检测可绕过 | PermissionManager.ts | 大小写、空白字符可绕过检测 |
|
||||
| 3 | 敏感路径检测逻辑错误 | PermissionManager.ts | `/etc/` 路径检测失效 |
|
||||
|
||||
### 3.2 功能缺陷 (🟡 中优先级)
|
||||
|
||||
| # | 问题 | 文件 | 影响 |
|
||||
|---|------|------|------|
|
||||
| 4 | 路径规范化使用 toLowerCase | SandboxManager.ts | 可能导致路径匹配错误 |
|
||||
| 5 | 权限请求超时后监听器泄漏 | PermissionManager.ts | 内存泄漏 |
|
||||
| 6 | 工作区 ID 生成可能冲突 | SandboxManager.ts | 高并发下可能产生重复 ID |
|
||||
| 7 | 文件操作未通过容器 | DockerSandbox.ts | 可能绕过沙箱隔离 |
|
||||
|
||||
### 3.3 代码质量 (🟢 低优先级)
|
||||
|
||||
| # | 问题 | 文件 | 影响 |
|
||||
|---|------|------|------|
|
||||
| 8 | 重复代码(路径验证、isPathPermission) | 多文件 | 维护成本 |
|
||||
| 9 | 硬编码超时值 | 多文件 | 可配置性差 |
|
||||
| 10 | 缺少持久化存储 | 全局 | 应用重启后状态丢失 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 改进建议
|
||||
|
||||
### 4.1 架构层面
|
||||
|
||||
1. **添加持久化层**
|
||||
- 使用 SQLite 存储工作区状态
|
||||
- 记录权限审批历史
|
||||
- 支持应用重启后恢复
|
||||
|
||||
2. **添加 WSL 和 Firejail 实现**
|
||||
- 当前只有 DockerSandbox 实现
|
||||
- WSL 实现 Windows 平台支持
|
||||
- Firejail 实现 Linux 轻量级沙箱
|
||||
|
||||
3. **添加指标收集**
|
||||
- 执行时间统计
|
||||
- 资源使用监控
|
||||
- 错误率追踪
|
||||
|
||||
### 4.2 安全层面
|
||||
|
||||
1. **增强命令验证**
|
||||
```typescript
|
||||
// 使用白名单 + 参数解析
|
||||
const ALLOWED_COMMANDS = new Set(['node', 'npm', 'git', ...]);
|
||||
const parsed = parseCommand(command);
|
||||
if (!ALLOWED_COMMANDS.has(parsed.name)) {
|
||||
throw new PermissionError(...);
|
||||
}
|
||||
```
|
||||
|
||||
2. **路径遍历防护**
|
||||
```typescript
|
||||
// 解析并验证真实路径
|
||||
const realPath = fs.realpathSync(path);
|
||||
if (!realPath.startsWith(workspaceRoot)) {
|
||||
throw new PermissionError(...);
|
||||
}
|
||||
```
|
||||
|
||||
3. **资源限制**
|
||||
```typescript
|
||||
// 添加磁盘配额检查
|
||||
async checkDiskQuota(workspace: Workspace): Promise<boolean> {
|
||||
const usage = await this.getDiskUsage(workspace.rootPath);
|
||||
return usage < parseSize(workspace.sandboxConfig.diskQuota || '10g');
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 代码层面
|
||||
|
||||
1. **统一日志**
|
||||
```typescript
|
||||
// 替换所有 console.error 为 log.error
|
||||
```
|
||||
|
||||
2. **提取共享工具**
|
||||
```typescript
|
||||
// 创建 src/shared/utils/pathUtils.ts
|
||||
export function normalizePath(path: string): string { ... }
|
||||
export function isPathPermission(type: PermissionType): boolean { ... }
|
||||
```
|
||||
|
||||
3. **添加配置常量**
|
||||
```typescript
|
||||
// src/shared/constants/sandbox.ts
|
||||
export const PERMISSION_TIMEOUT = 60000;
|
||||
export const DEFAULT_MEMORY_LIMIT = '2g';
|
||||
export const WORKSPACE_ID_PREFIX = 'workspace-';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 优先修复项
|
||||
|
||||
### P0 - 立即修复(安全漏洞)
|
||||
|
||||
1. **修复 Docker 命令注入**
|
||||
- 文件: `DockerSandbox.ts`
|
||||
- 使用 `spawn` 替代字符串拼接
|
||||
- 添加镜像名验证
|
||||
|
||||
2. **修复危险命令检测绕过**
|
||||
- 文件: `PermissionManager.ts`
|
||||
- 规范化命令(去除空白、转小写)
|
||||
- 使用正则表达式边界匹配
|
||||
|
||||
3. **修复敏感路径检测**
|
||||
- 文件: `PermissionManager.ts`
|
||||
- 修复 `/etc/` 路径检测逻辑
|
||||
|
||||
### P1 - 尽快修复(功能缺陷)
|
||||
|
||||
4. **修复路径规范化**
|
||||
- 文件: `SandboxManager.ts`
|
||||
- 移除 `toLowerCase()` 调用
|
||||
|
||||
5. **修复监听器泄漏**
|
||||
- 文件: `PermissionManager.ts`
|
||||
- 超时后移除事件监听器
|
||||
|
||||
6. **改进工作区 ID 生成**
|
||||
- 文件: `SandboxManager.ts`
|
||||
- 使用 UUID 或加密随机数
|
||||
|
||||
### P2 - 计划修复(代码质量)
|
||||
|
||||
7. **提取共享工具函数**
|
||||
8. **统一日志使用**
|
||||
9. **添加配置常量**
|
||||
10. **添加单元测试**
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 Harness 架构对齐检查
|
||||
|
||||
| 检查项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| CP1 任务确认 | ✅ | 有参数验证 |
|
||||
| CP2 规划分解 | ⚠️ | 部分实现,缺少显式的规划阶段 |
|
||||
| CP3 执行实现 | ✅ | 完整实现 |
|
||||
| CP4 质量门禁 | ⚠️ | 类型定义存在,但未实现门禁逻辑 |
|
||||
| CP5 审查完成 | ⚠️ | 有 metrics 类型,但未实现收集 |
|
||||
|
||||
**建议**: 在 WorkspaceManager 中添加显式的 CP 工作流支持,包括:
|
||||
- 门禁检查机制
|
||||
- 指标收集
|
||||
- 状态持久化
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试建议
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
```typescript
|
||||
// 测试危险命令检测
|
||||
describe('PermissionManager.checkDangerousOperation', () => {
|
||||
it('should detect dangerous commands regardless of case', () => {
|
||||
expect(pm.checkDangerousOperation('command:execute', 'SUDO rm -rf /')).toBe(true);
|
||||
expect(pm.checkDangerousOperation('command:execute', 'sUdO ls')).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect commands with extra whitespace', () => {
|
||||
expect(pm.checkDangerousOperation('command:execute', 'sudo rm -rf /')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// 测试路径验证
|
||||
describe('SandboxManager.validatePathInWorkspace', () => {
|
||||
it('should reject paths outside workspace', () => {
|
||||
expect(() => sm.validatePathInWorkspace(workspace, '/etc/passwd')).toThrow();
|
||||
});
|
||||
|
||||
it('should handle path traversal attempts', () => {
|
||||
expect(() => sm.validatePathInWorkspace(workspace, '../etc/passwd')).toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 集成测试
|
||||
|
||||
```typescript
|
||||
describe('DockerSandbox Integration', () => {
|
||||
it('should create and destroy workspace', async () => {
|
||||
const sandbox = new DockerSandbox(config);
|
||||
await sandbox.init();
|
||||
const workspace = await sandbox.createWorkspace('test-session');
|
||||
expect(workspace).toBeDefined();
|
||||
await sandbox.destroyWorkspace('test-session');
|
||||
});
|
||||
|
||||
it('should execute commands in container', async () => {
|
||||
const result = await sandbox.execute('test-session', 'echo', ['hello']);
|
||||
expect(result.stdout.trim()).toBe('hello');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 结论
|
||||
|
||||
沙箱实现代码整体架构合理,类型定义完整,错误处理得当。但存在若干安全漏洞需要立即修复,特别是命令注入和危险命令检测绕过问题。建议按优先级修复问题,并补充单元测试和集成测试。
|
||||
|
||||
**下一步行动**:
|
||||
1. 修复 P0 安全漏洞
|
||||
2. 添加单元测试
|
||||
3. 实现 WSL/Firejail 支持
|
||||
4. 添加持久化存储
|
||||
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