17-sha-xiang-an-quan-ji-zhi
17-sha-xiang-an-quan-ji-zhi

17-sha-xiang-an-quan-ji-zhi

Claude Code 的沙箱机制是纵深防御体系的第三层——即使 AI 生成了恶意命令,或权限审批被绕过,操作系统级别的沙箱约束仍能阻止危险行为。与权限系统(回答”这条命令能不能执行”)不同,沙箱决定”执行时能做到什么程度”。两者共同构成从应用层到 OS 层的双重安全屏障。

Sources: sandbox.mdx

架构概述:命令执行链路

一条 Bash 命令从用户输入到沙箱包裹,经历完整的验证链路:

flowchart TD
    A[用户输入] --> B[BashTool.call]
    B --> C[shouldUseSandbox input]
    C --> D{全局开关检查}
    D -->|未启用| E[跳过沙箱]
    D -->|启用| F{显式禁用检查}
    F -->|dangerouslyDisableSandbox| G{策略允许?}
    F -->|正常流程| H{排除列表检查}
    H -->|匹配| E
    H -->|不匹配| I[进入沙箱]
    G -->|允许| E
    G -->|禁止| I
    I --> J[Shell.exec shouldUseSandbox=true]
    J --> K[SandboxManager.wrapWithSandbox]
    K --> L[sandbox-exec bwrap]
    L --> M[spawn wrapped_command]

关键判定函数 shouldUseSandbox() 位于 src/tools/BashTool/shouldUseSandbox.ts,执行四层检查逻辑:

export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
  // 1. 全局未启用 → 跳过
  if (!SandboxManager.isSandboxingEnabled()) return false

  // 2. 显式禁用 + 策略允许 → 跳过
  if (input.dangerouslyDisableSandbox && 
      SandboxManager.areUnsandboxedCommandsAllowed()) return false

  // 3. 无命令 → 跳过
  if (!input.command) return false

  // 4. 匹配排除列表 → 跳过
  if (containsExcludedCommand(input.command)) return false

  // 5. 其他情况 → 必须沙箱化
  return true
}

Sources: shouldUseSandbox.ts

沙箱判定逻辑:排除命令匹配机制

containsExcludedCommand() 的匹配机制是理解沙箱行为的核心。该函数支持三种匹配模式,不仅检查命令本身,还会对复合命令进行拆分迭代检查:

模式 语法 匹配行为
精确匹配 npm run lint 完全相等才匹配
前缀匹配 npm run test:* 前缀 + 空格或完全相等
通配符 docker* 使用 glob 模式匹配

对于复合命令(如 docker ps && curl evil.com),系统会先通过 splitCommand_DEPRECATED() 拆分为子命令,逐一检查每个子命令是否匹配排除模式。

更重要的是,算法会迭代剥离环境变量前缀(FOO=bar bazel ...)和包装命令(timeout 30 bazel ...),直到不动点为止。这种双重剥离确保了:

// 迭代生成候选命令列表
const candidates = [trimmed]
const seen = new Set(candidates)
while (startIdx < candidates.length) {
  const envStripped = stripAllLeadingEnvVars(cmd, BINARY_HIJACK_VARS)
  if (!seen.has(envStripped)) candidates.push(envStripped)
  const wrapperStripped = stripSafeWrappers(cmd)
  if (!seen.has(wrapperStripped)) candidates.push(wrapperStripped)
}

这意味着 FOO=bar bazel build 会匹配 bazel:* 排除规则,即使原始命令被包装为 timeout 300 FOO=bar bazel build。

Sources: shouldUseSandbox.ts

平台实现差异

macOS:Seatbelt(sandbox-exec)

macOS 使用 Apple 原生的 Seatbelt 沙箱,通过 sandbox-exec 命令包裹原始命令:

sandbox-exec -p <profile> -- <original_command>

Seatbelt profile 是基于配置中的网络/文件系统规则动态生成的,在内核级别强制执行约束。网络隔离通过代理端口拦截 HTTP/HTTPS 请求实现,Unix socket 可单独配置允许路径。

Linux:bubblewrap + seccomp

Linux 使用 bubblewrap(bwrap)创建命名空间隔离,配合 seccomp 过滤系统调用:

依赖项 作用
bubblewrap 创建 mount/PID/network 命名空间
socat 网络代理(HTTP/SOCKS)
ripgrep 搜索工具(内置支持)
libseccomp / seccomp filter 过滤 Unix socket 系统调用

bwrap 与 Seatbelt 的关键差异:

  • 不支持 glob 路径模式:Linux 上带 glob 的权限规则会触发警告(getLinuxGlobPatternWarnings())
  • Unix socket 无法按路径过滤:seccomp 只能全允许或全拒绝,无法像 macOS 那样按路径放行

Sources: sandbox-adapter.ts

配置模型

沙箱配置定义在 src/entrypoints/sandboxTypes.ts 中,通过 Zod schema 验证。完整的配置结构:

{
  "sandbox": {
    "enabled": true,                      // 主开关
    "autoAllowBashIfSandboxed": true,     // 沙箱中命令自动放行(跳过审批)
    "allowUnsandboxedCommands": true,     // 是否允许 dangerouslyDisableSandbox
    "failIfUnavailable": false,           // 依赖缺失时是否报错退出

    "network": {
      "allowedDomains": ["github.com"],   // 网络白名单
      "deniedDomains": [],                // 网络黑名单
      "allowLocalBinding": true,          // 允许 localhost 绑定
      "httpProxyPort": 8888               // HTTP 代理端口(MITM)
    },

    "filesystem": {
      "allowWrite": ["~/projects"],       // 额外可写路径
      "denyWrite": ["~/.ssh"],            // 禁止写入路径
      "denyRead": [],                     // 禁止读取路径
      "allowRead": []                     // 在 denyRead 中重新放行
    },

    "excludedCommands": ["docker", "npm:*"]  // 不走沙箱的命令
  }
}

Sources: sandboxTypes.ts

初始化流程与动态配置

沙箱的初始化发生在 REPL 或 SDK 启动时,通过 SandboxManager.initialize() 完成:

sequenceDiagram
    participant Main as main.tsx
    participant SM as SandboxManager
    participant Base as BaseSandboxManager
    participant Settings as Settings System

    Main->>SM: initialize(sandboxAskCallback)
    SM->>SM: detectWorktreeMainRepoPath()
    SM->>Settings: getSettings_DEPRECATED()
    SM->>SM: convertToSandboxRuntimeConfig()
    SM->>Base: BaseSandboxManager.initialize(config)
    SM->>Settings: settingsChangeDetector.subscribe()
    Note over SM: 动态监听设置变更

convertToSandboxRuntimeConfig() 是关键的配置转换函数,它从权限规则中提取约束并构建运行时配置:

// 从 WebFetch 权限规则提取域名
const allowedDomains: string[] = []
for (const ruleString of permissions.allow || []) {
  const rule = permissionRuleValueFromString(ruleString)
  if (rule.toolName === WEB_FETCH_TOOL_NAME && 
      rule.ruleContent?.startsWith('domain:')) {
    allowedDomains.push(rule.ruleContent.substring('domain:'.length))
  }
}

// 从 Edit/Read 权限规则提取文件系统路径
const allowWrite: string[] = ['.', getClaudeTempDir()]

Sources: sandbox-adapter.ts

安全加固:自动注入的防护规则

sandbox-adapter.ts 中硬编码了多项安全加固措施,这些规则不会被用户的配置文件覆盖:

保护目标 规则类型 说明
settings.json denyWrite 防止沙箱逃逸到配置文件
.claude/skills denyWrite 防止技能注入攻击
HEAD, objects, refs 条件 denyWrite 防护 bare git repo 攻击
项目目录 allowWrite 自动放行当前项目

Bare Git Repo 攻击防御

这是一个典型攻击向量:攻击者在 cwd 创建 HEAD + objects/ + refs/ 目录,伪装成 git bare repo,然后配置恶意 hooks。当 Claude 运行 unsandboxed git 时触发攻击。

防御措施:

// 如果文件已存在,直接 denyWrite
const bareGitRepoFiles = ['HEAD', 'objects', 'refs', 'hooks', 'config']
for (const gitFile of bareGitRepoFiles) {
  try {
    statSync(p)
    denyWrite.push(p)  // 只读绑定
  } catch {
    bareGitRepoScrubPaths.push(p)  // 事后清理
  }
}

Sources: sandbox-adapter.ts

dangerouslyDisableSandbox 的设计权衡

这个参数的命名本身就是警示——它代表”危险地禁用沙箱”,而非简单的配置选项。设计包含双重保险机制:

// 调用侧:模型可以在 BashTool inputSchema 中请求禁用沙箱
// 策略侧:管理员可通过 allowUnsandboxedCommands: false 完全禁止

if (input.dangerouslyDisableSandbox && 
    SandboxManager.areUnsandboxedCommandsAllowed()) {
  return false  // 只有策略允许时才真正跳过沙箱
}

当 autoAllowBashIfSandboxed: true 时,沙箱中的命令会自动获得执行许可,无需逐条审批。这基于信任假设:如果 OS 级沙箱已经限制了命令的能力,应用层的逐条审批就变得多余。

Sources: sandbox.mdx

沙箱执行与清理

Shell.ts 中的 exec() 函数是命令执行的最终入口:

if (shouldUseSandbox) {
  commandString = await SandboxManager.wrapWithSandbox(
    commandString,
    sandboxBinShell,
    undefined,
    abortSignal,
  )
  // 创建沙箱临时目录
  await fs.mkdir(sandboxTmpDir, { mode: 0o700 })
}

// 命令执行后清理
void shellCommand.result.then(async result => {
  if (shouldUseSandbox) {
    SandboxManager.cleanupAfterCommand()
  }
})

cleanupAfterCommand() 执行两项清理:

  1. 调用底层清理:bwrap 会在当前目录留下 0 字节的 mount-point 文件(如 .bashrc)
  2. 清理 planted bare repo 文件:防止 planted 文件被 unsandboxed git 读取

Sources: Shell.ts, sandbox-adapter.ts

违规处理与审计

当命令尝试违反沙箱约束时,系统通过以下机制处理:

flowchart TD
    A[命令执行] --> B{违反沙箱约束?}
    B -->|是| C[运行时捕获违规事件]
    C --> D[annotateStderrWithSandboxFailures]
    D --> E[注入 <sandbox_violations> 标签]
    E --> F[SandboxViolationStore 持久化]
    F --> G[UI 层 removeSandboxViolationTags]
    G --> H[清理显示]

removeSandboxViolationTags() 用于清理错误消息中的违规标记:

export function removeSandboxViolationTags(text: string): string {
  return text.replace(/<sandbox_violations>[\s\S]*?<\/sandbox_violations>/g, '')
}

Sources: sandbox-ui-utils.ts, sandbox-adapter.ts

平台支持矩阵

特性 macOS Linux WSL2
沙箱引擎 sandbox-exec (Seatbelt) bubblewrap + seccomp 同 Linux
文件 glob 模式 ✅ 完整支持 ⚠️ 警告(不支持) ⚠️ 警告
Unix socket 按路径 ✅ 支持 ❌ 不支持 ❌ 不支持
依赖检查 ripgrep bwrap + socat + ripgrep 同 Linux

依赖检查状态

const depCheck = SandboxManager.checkDependencies()
// depCheck.errors: 缺失的必需依赖
// depCheck.warnings: 可选依赖缺失警告

Sources: SandboxDependenciesTab.tsx

交互式配置界面

Claude Code 提供 /sandbox 命令用于交互式配置:

/sandbox              # 打开交互式配置菜单
/sandbox exclude "npm run test:*"  # 添加排除命令

配置界面包含三个标签页:

  • Mode:选择沙箱模式(auto-allow / regular / disabled)
  • Overrides:配置 unsandboxed fallback 策略
  • Config:查看当前配置状态和 glob 模式警告

Sources: sandbox-toggle.tsx, SandboxSettings.tsx

权限系统与沙箱的关系

理解两者的区别至关重要:

维度 权限系统 沙箱机制
层级 应用层 OS 层
检查时机 工具调用前 进程执行时
控制粒度 按命令/路径 按系统调用
可绕过性 可能被 prompt injection 绕过 内核级强制执行
配置方式 规则匹配 系统级约束

权限系统是”软约束”(依赖模型遵从),沙箱是”硬约束”(即使 AI 生成了恶意命令也无法突破)。两者构成纵深防御的两层。

Sources: why-safety-matters.mdx, permission-model.mdx

下一步

  • 权限模型与规则引擎:了解 Allow/Ask/Deny 三级权限体系
  • Auto Mode 自动模式:了解 AI 分类器驱动的自主执行模式
  • Remote Control 远程控制:了解沙箱在远程场景中的应用

相关条目

  • 15-quan-xian-mo-xing-yu-gui-ze-yin-qing
  • 13-computer-use-dian-nao-cao-kong

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注