Skip to content

第15章 沙箱、隔离与防御性编程

"Security is not a feature — it's a constraint that shapes every design decision."

本章要点

  • 沙箱是权限模型的物理执行层——权限决定"允许做什么",沙箱保证"只能做什么"
  • OS 级沙箱(macOS Seatbelt、Linux seccomp)提供最强隔离
  • 命令过滤(正则匹配 + 黑白名单)是最常用的轻量方案
  • 纵深防御:权限模型 + 沙箱 + 命令过滤 + 用户确认,多层叠加

15.1 为什么需要沙箱

权限模型(第14章)解决的是"是否允许"的问题。但权限判断是在 Harness 层面做的——如果模型想出了绕过 Harness 的方式呢?

模型意图: 读取 /etc/passwd
权限检查: Read 工具不允许读取系统文件 ✓ 已拦截
模型意图: 通过 Bash 执行 cat /etc/passwd
权限检查: Bash 工具允许执行命令... 但命令内容包含敏感路径

沙箱在操作系统层面提供了第二道防线——即使 Harness 层的检查被绕过,OS 级限制仍然生效。

15.2 OS 级沙箱

macOS Seatbelt

Claude Code 在 macOS 上使用 Seatbelt(sandbox-exec)来限制 Bash 命令的能力:

scheme
;; 简化的 Seatbelt profile
(version 1)
(deny default)                          ; 默认拒绝一切

(allow file-read*
  (subpath "/Users/yyt/project"))       ; 只允许读项目目录

(allow file-write*
  (subpath "/Users/yyt/project")        ; 只允许写项目目录
  (subpath "/tmp"))                     ; 和临时目录

(deny file-read*
  (subpath "/Users/yyt/.ssh")           ; 明确禁止读 SSH 密钥
  (subpath "/Users/yyt/.aws"))          ; 明确禁止读 AWS 凭证

(allow network-outbound
  (remote tcp "localhost:*"))           ; 只允许本地网络
(deny network-outbound)                 ; 禁止外部网络

执行命令时包裹在 sandbox 中:

typescript
async function executeSandboxed(command: string): Promise<ExecResult> {
  const profile = generateSeatbeltProfile(context)
  return exec(`sandbox-exec -f ${profile} bash -c "${command}"`)
}

Linux 方案

Linux 环境下有多种选择:

bash
# Docker 容器隔离
docker run --rm \
  --network none \                    # 禁止网络
  --read-only \                       # 只读文件系统
  -v /project:/workspace:rw \         # 只挂载项目目录
  --memory 512m \                     # 限制内存
  --cpus 1 \                          # 限制 CPU
  agent-sandbox bash -c "$COMMAND"

# 或者用 firejail(更轻量)
firejail --noprofile \
  --whitelist=/project \
  --net=none \
  bash -c "$COMMAND"

沙箱方案对比

方案隔离强度性能开销适用场景平台
macOS Seatbelt极低(内核级)Claude Code 桌面macOS
Docker很高中(容器启动 ~200ms)CI/CD、云端部署Linux
firejail低(进程级)轻量级 Linux 隔离Linux
seccomp-bpf极高极低精细系统调用过滤Linux
纯代码层限制无法使用 OS 沙箱时的兜底任意

选择建议:桌面 Agent(如 Claude Code)用 Seatbelt/firejail,云端 Agent 用 Docker,高安全场景用 microVM(如 Firecracker)。 无论选哪种,都应该在外层叠加代码级验证作为兜底。

15.3 文件系统隔离

限制 Agent 只能访问项目目录及其子目录:

typescript
function validateFilePath(requestedPath: string, projectRoot: string): boolean {
  const resolved = path.resolve(requestedPath)
  const root = path.resolve(projectRoot)

  // 必须在项目目录内
  if (!resolved.startsWith(root + path.sep) && resolved !== root) {
    return false
  }

  // 黑名单:即使在项目内也不能访问
  const BLOCKED_PATTERNS = [
    /\.env$/,              // 环境变量文件
    /\.env\..+$/,          // .env.local, .env.production
    /credentials/i,        // 凭证文件
    /secrets/i,            // 密钥文件
    /\.pem$/,              // 证书
    /\.key$/,              // 私钥
  ]

  const basename = path.basename(resolved)
  return !BLOCKED_PATTERNS.some(p => p.test(basename))
}

Claude Code 的 Read 工具要求路径必须是绝对路径,并在执行前验证路径合法性。

15.4 命令过滤

Bash 工具是最危险的工具——它能执行任意命令。必须有过滤机制。

黑名单模式

typescript
const BLOCKED_COMMANDS = [
  /\brm\s+-rf\s+[\/~]/,         // rm -rf / 或 rm -rf ~
  /\bcurl\b.*\|\s*bash/,         // curl | bash(远程代码执行)
  /\bchmod\s+777/,               // 过于宽松的权限
  /\bsudo\b/,                    // 提权操作
  /\bkill\s+-9\s+1\b/,           // 杀 init 进程
  /\bdd\b.*of=\/dev/,            // 直接写磁盘设备
  /\bmkfs\b/,                    // 格式化文件系统
  />\s*\/etc\//,                 // 重定向写入系统目录
  /\bssh\b.*@/,                  // SSH 到远程主机
]

function isCommandBlocked(command: string): boolean {
  return BLOCKED_COMMANDS.some(pattern => pattern.test(command))
}

白名单模式(更安全)

只允许已知安全的命令前缀:

typescript
const ALLOWED_PREFIXES = [
  'node', 'npm', 'npx', 'yarn', 'pnpm',
  'python', 'pip', 'pytest',
  'cargo', 'rustc',
  'git', 'grep', 'find', 'ls', 'cat', 'head', 'tail',
  'echo', 'pwd', 'which', 'env',
]

function isCommandAllowed(command: string): boolean {
  const firstWord = command.trim().split(/\s/)[0]
  return ALLOWED_PREFIXES.includes(firstWord)
}

Claude Code 的混合方案

Claude Code 结合两种模式:

  1. 用户可通过权限规则设置允许/禁止的命令模式
  2. 系统内置一组硬编码的危险命令黑名单
  3. 未匹配任何规则的命令需要用户确认

15.5 进程资源限制

防止 Agent 启动的进程消耗过多资源:

typescript
const PROCESS_LIMITS = {
  timeout: 120_000,        // 单个命令最长 2 分钟
  maxOutputSize: 1_000_000, // 输出最大 1MB
  maxConcurrent: 5,         // 最多 5 个并发进程
}

async function executeWithLimits(
  command: string
): Promise<ExecResult> {
  const controller = new AbortController()
  const timer = setTimeout(
    () => controller.abort(),
    PROCESS_LIMITS.timeout
  )

  try {
    const result = await exec(command, {
      signal: controller.signal,
      maxBuffer: PROCESS_LIMITS.maxOutputSize,
    })
    return result
  } catch (e) {
    if (e.name === 'AbortError') {
      return { exitCode: -1, output: 'Command timed out' }
    }
    throw e
  } finally {
    clearTimeout(timer)
  }
}

15.6 网络隔离

Agent 是否应该能访问网络?这取决于使用场景:

  • 代码编辑 Agent:通常不需要网络,禁止更安全
  • 研究 Agent:需要搜索和获取网页,但应限制目标域名
  • 部署 Agent:需要 SSH/SCP 到服务器,但只限特定主机
typescript
const NETWORK_POLICY = {
  allowLocalhost: true,     // 本地开发服务器
  allowedDomains: [
    'api.github.com',       // GitHub API
    'registry.npmjs.org',   // npm registry
  ],
  blockedPorts: [22, 3306, 5432, 6379],  // SSH, MySQL, PostgreSQL, Redis
}

15.7 纵深防御

单层防御不可靠。成熟的 Agent 系统采用纵深防御:

更详细地说:

Layer 1: 权限模型
  ↓ 这个操作是否被允许?
Layer 2: 命令过滤
  ↓ 这个具体命令是否安全?
Layer 3: 路径验证
  ↓ 目标文件是否在允许范围内?
Layer 4: OS 沙箱
  ↓ 即使通过了前三层,OS 层面还有限制
Layer 5: 用户确认
  ↓ 对于高风险操作,最终由人类决定

每一层独立工作——即使某一层被绕过,后续层仍然提供保护。

Claude Code 的实际防御链路:

用户说 "删除 node_modules"
  → 权限检查: Bash 工具在当前模式下是否允许?
  → 命令分析: rm -rf 是否匹配危险命令模式?
  → 可逆性评估: 这是一个可逆操作吗?(是,可以 npm install 恢复)
  → 决策: 需要用户确认
  → 用户确认后执行
  → 沙箱内执行: 只能删除项目目录内的文件

15.8 防御性编程实践

工具实现的防御原则

typescript
// ❌ 信任模型输入
async function deleteFile(path: string) {
  await fs.unlink(path)  // 如果 path 是 /etc/passwd 呢?
}

// ✅ 验证一切输入
async function deleteFile(path: string, context: Context) {
  const resolved = path.resolve(path)

  // 1. 路径必须在项目内
  if (!resolved.startsWith(context.projectRoot)) {
    throw new Error('Path outside project directory')
  }

  // 2. 不能是受保护的文件
  if (isProtectedFile(resolved)) {
    throw new Error('Cannot delete protected file')
  }

  // 3. 记录操作日志
  audit.log('file.delete', { path: resolved, user: context.userId })

  await fs.unlink(resolved)
}

最小权限原则

每个工具只应拥有完成其功能所需的最小权限:

  • Read 工具:只有读权限,不能写
  • Edit 工具:只能修改已有文件的部分内容,不能创建新文件
  • Write 工具:可以创建新文件,但需要先 Read 过已有文件
  • Bash 工具:在沙箱中执行,有超时限制

失败安全

当安全检查出错时,应该拒绝而非允许

typescript
function checkPermission(action: Action): boolean {
  try {
    return evaluateRules(action)
  } catch (error) {
    // 安全检查本身出错时,默认拒绝
    log.warn('Permission check failed, denying by default', error)
    return false
  }
}

15.9 安全 vs 可用性的平衡

过度安全会让 Agent 变得无用:

场景: Agent 需要安装一个 npm 包
过度安全: "禁止执行 npm install,可能下载恶意代码"
合理安全: "允许 npm install,但禁止 npm install --global"

平衡的原则:

  • 开发环境宽松,生产环境严格
  • 可逆操作宽松,不可逆操作严格
  • 用户在场宽松,无人值守严格
  • 已知命令宽松,未知命令严格

15.10 本章小结

沙箱隔离是 Agent 安全的最后一道物理防线:

  1. OS 沙箱 提供最强隔离——Seatbelt、seccomp、Docker
  2. 文件系统隔离 限制 Agent 只能访问项目目录
  3. 命令过滤 拦截已知危险的 Shell 命令
  4. 资源限制 防止 Agent 进程失控
  5. 纵深防御 多层叠加,任何一层被绕过都有后续保护
  6. 失败安全 安全检查出错时默认拒绝
  7. 平衡可用性 安全不是目的,可控的能力才是

下一章我们将从单 Agent 扩展到多 Agent,看看如何协调多个 Agent 高效协作。

基于 VitePress 构建