Appearance
第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 结合两种模式:
- 用户可通过权限规则设置允许/禁止的命令模式
- 系统内置一组硬编码的危险命令黑名单
- 未匹配任何规则的命令需要用户确认
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 安全的最后一道物理防线:
- OS 沙箱 提供最强隔离——Seatbelt、seccomp、Docker
- 文件系统隔离 限制 Agent 只能访问项目目录
- 命令过滤 拦截已知危险的 Shell 命令
- 资源限制 防止 Agent 进程失控
- 纵深防御 多层叠加,任何一层被绕过都有后续保护
- 失败安全 安全检查出错时默认拒绝
- 平衡可用性 安全不是目的,可控的能力才是
下一章我们将从单 Agent 扩展到多 Agent,看看如何协调多个 Agent 高效协作。