BashTool 权限系统深度解析
基于 Claude Code 源码
/home/sujie/dev/github/claude-code-source-snap/claude-code/src/tools/BashTool/核心文件总行数:bashPermissions.ts(2621行),bashSecurity.ts(~2600行),pathValidation.ts(1303行),readOnlyValidation.ts(1600+行)
1. 系统概览
BashTool 的权限系统是一个多层防御、深度分析的命令安全框架。它不依赖单一检查,而是通过至少 7 层独立验证来决定每条命令的命运:allow(自动放行)、ask(请求用户确认)或 deny(直接拒绝)。
整个系统围绕一个核心问题设计:这条 shell 命令到底在做什么? 为了回答这个问题,系统同时使用了:
- tree-sitter AST 解析(新版,主路径)
- shell-quote 正则解析(legacy 回退路径)
- 20+ 个独立的正则验证器
- 机器学习分类器(LLM-based classifier)
- 用户自定义的 allow/deny/ask 规则
2. 架构与决策流
2.1 文件职责划分
| 文件 | 职责 |
|---|---|
BashTool.tsx | 工具定义、入口、执行逻辑 |
bashPermissions.ts (2621行) | 主权限检查入口 bashToolHasPermission(),规则匹配,子命令拆分 |
bashSecurity.ts (~2600行) | 命令安全验证器链 — 20+ 个独立 validator |
bashCommandHelpers.ts | 管道操作符 (` |
pathValidation.ts (1303行) | 路径约束检查 — 命令操作的文件是否在允许目录内 |
readOnlyValidation.ts (1600+行) | 只读命令白名单 — 基于 flag 的精细化验证 |
modeValidation.ts | 模式相关权限 (acceptEdits 模式自动放行文件系统操作) |
sedValidation.ts (684行) | sed 命令的专项安全检查 |
destructiveCommandWarning.ts | 破坏性命令的 UI 警告提示 |
commandSemantics.ts | 命令退出码的语义解释 |
2.2 权限决策流程
┌─────────────────────────┐
│ bashToolHasPermission() │
│ (bashPermissions.ts) │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Step 0: AST 解析 │
│ parseForSecurity() │
│ tree-sitter-bash │
└────────────┬────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────┐ ┌────────────────┐
│ kind: 'simple' │ │ kind:'too- │ │ kind:'parse- │
│ → 继续检查 │ │ complex' │ │ unavailable' │
│ │ │ → ask (安全) │ │ → 回退legacy │
└───────┬────────┘ └──────────────┘ └────────────────┘
│
┌────────▼────────┐
│ checkSemantics()│ ← AST 语义检查
│ (ast.ts:2213) │ ← zsh builtins, eval, etc.
└────────┬────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 1: 沙箱自动放行检查 │
│ checkSandboxAutoAllow() │
│ → 尊重显式 deny/ask 规则 │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 2: 精确匹配规则检查 │
│ bashToolCheckExactMatchPermission() │
│ deny > ask > allow > passthrough │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 3: LLM 分类器 (并行) │
│ classifyBashCommand() — deny + ask 同时检查 │
│ high confidence → deny/ask │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 4: 管道操作符检查 │
│ checkCommandOperatorPermissions() │
│ → 拆分管道段,递归检查每个段 │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 5: Legacy misparsing gate │
│ bashCommandIsSafeAsync() (仅当 AST 不可用) │
│ → 20+ 个安全验证器 │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 6: 子命令拆分 + 逐个检查 │
│ splitCommand() → bashToolCheckPermission() × N │
│ cd+git 组合检查、子命令数量上限 (50) │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ Step 7: 最终决策 │
│ 全部 allow → allow │
│ 有 deny → deny │
│ 有 ask/passthrough → ask + 建议规则 │
└──────────────────────────────────────────────────┘
3. AST/命令解析机制
3.1 tree-sitter AST 解析(主路径)
核心在 src/utils/bash/ast.ts,使用 tree-sitter-bash WASM 模块解析命令。
// ast.ts:42-46
export type ParseForSecurityResult =
| { kind: 'simple'; commands: SimpleCommand[] }
| { kind: 'too-complex'; reason: string; nodeType?: string }
| { kind: 'parse-unavailable' }
设计哲学是 fail-closed:任何不在显式白名单中的 AST 节点类型都会导致 too-complex 结果,强制用户确认。
// ast.ts:54-59 — 结构性节点(允许递归遍历)
const STRUCTURAL_TYPES = new Set([
'program', 'list', 'pipeline', 'redirected_statement',
])
// ast.ts:186-205 — 危险节点类型(立即返回 too-complex)
const DANGEROUS_TYPES = new Set([
'command_substitution', // $()
'process_substitution', // <() >()
'expansion', // ${}
'simple_expansion', // $VAR (未跟踪)
'brace_expression', // {a,b}
'subshell', // (cmd)
'compound_statement', // { cmd; }
'for_statement', // for/do/done
'while_statement', // while/do/done
'if_statement', // if/then/fi
'case_statement', // case/esac
'function_definition', // function foo()
'test_command', // [[ ]]
'ansi_c_string', // $'...'
'translated_string', // $"..."
'heredoc_redirect', // <<EOF
'herestring_redirect', // <<<
])
SimpleCommand 结构:
// ast.ts:31-40
export type SimpleCommand = {
argv: string[] // 命令名+参数,引号已解析
envVars: { name: string; value: string }[] // 前置 VAR=val
redirects: Redirect[] // 重定向操作符和目标
text: string // 原始源码片段
}
3.2 预检查(Pre-checks)
在 AST 遍历之前,parseForSecurityFromAst() 运行一系列字符级预检查,检测 tree-sitter 和 bash 之间的分歧:
// ast.ts:408-437
if (CONTROL_CHAR_RE.test(cmd)) // 控制字符
return { kind: 'too-complex', ... }
if (UNICODE_WHITESPACE_RE.test(cmd)) // Unicode 空白
return { kind: 'too-complex', ... }
if (BACKSLASH_WHITESPACE_RE.test(cmd)) // 反斜杠转义空白
return { kind: 'too-complex', ... }
if (ZSH_TILDE_BRACKET_RE.test(cmd)) // zsh ~[ 扩展
return { kind: 'too-complex', ... }
if (ZSH_EQUALS_EXPANSION_RE.test(cmd)) // zsh =cmd 扩展
return { kind: 'too-complex', ... }
if (BRACE_WITH_QUOTE_RE.test(...)) // 花括号+引号混淆
return { kind: 'too-complex', ... }
3.3 Legacy 回退路径
当 tree-sitter 不可用时,回退到 bashCommandIsSafe_DEPRECATED()(bashSecurity.ts:2257),使用 shell-quote 库 + 20+ 个正则验证器。
4. 命令分类规则
4.1 安全验证器链(bashSecurity.ts)
系统维护了一个按顺序执行的验证器列表。每个验证器返回 PermissionResult:
// bashSecurity.ts:2308-2378
const earlyValidators = [
validateEmpty, // 空命令 → allow
validateIncompleteCommands, // 不完整片段 → ask
validateSafeCommandSubstitution, // 安全 heredoc $() → allow
validateGitCommit, // git commit -m "msg" → allow
]
const validators = [
validateJqCommand, // jq system() 函数检查
validateObfuscatedFlags, // 混淆标志检测 ($'', ""-, etc.)
validateShellMetacharacters, // 元字符在引号中的检查
validateDangerousVariables, // 变量在重定向/管道中的使用
validateCommentQuoteDesync, // # 注释中的引号不同步
validateQuotedNewline, // 引号内换行+注释行攻击
validateCarriageReturn, // \r 的解析分歧
validateNewlines, // 换行分隔多个命令
validateIFSInjection, // $IFS 注入
validateProcEnvironAccess, // /proc/*/environ 访问
validateDangerousPatterns, // 反引号、$()、>(), <() 等
validateRedirections, // < > 重定向
validateBackslashEscapedWhitespace, // 反斜杠转义空白
validateBackslashEscapedOperators, // 反斜杠转义操作符 (\; \| 等)
validateUnicodeWhitespace, // Unicode 空白字符
validateMidWordHash, // 词中 # (shell-quote 分歧)
validateBraceExpansion, // 花括号展开 {a,b}
validateZshDangerousCommands, // zmodload, emulate 等
validateMalformedTokenInjection, // 畸形 token + 命令分隔符
]
4.2 验证器的 misparsing 分类
验证器分为两类:
- misparsing 验证器:检测 shell-quote 解析器和 bash 行为不一致的情况。返回结果附带
isBashSecurityCheckForMisparsing: true,在权限流程中会被提前拦截。 - non-misparsing 验证器(
validateNewlines,validateRedirections):正常的命令结构检查,不标记 misparsing 标志。
// bashSecurity.ts:2343-2346
const nonMisparsingValidators = new Set([
validateNewlines,
validateRedirections,
])
4.3 危险模式检测
命令替换检测(bashSecurity.ts:16-41):
const COMMAND_SUBSTITUTION_PATTERNS = [
{ pattern: /<\(/, message: 'process substitution <()' },
{ pattern: />\(/, message: 'process substitution >()' },
{ pattern: /=\(/, message: 'Zsh process substitution =()' },
{ pattern: /\$\(/, message: '$() command substitution' },
{ pattern: /\$\{/, message: '${} parameter substitution' },
{ pattern: /\$\[/, message: '$[] legacy arithmetic expansion' },
{ pattern: /~\[/, message: 'Zsh-style parameter expansion' },
// ... 更多
]
Zsh 危险命令(bashSecurity.ts:45-74):
const ZSH_DANGEROUS_COMMANDS = new Set([
'zmodload', 'emulate',
'sysopen', 'sysread', 'syswrite', 'sysseek',
'zpty', 'ztcp', 'zsocket', 'mapfile',
'zf_rm', 'zf_mv', 'zf_ln', 'zf_chmod', 'zf_chown', 'zf_mkdir', 'zf_rmdir',
])
5. 权限决策逻辑 (allow/deny/ask)
5.1 规则匹配系统
规则有三种类型:exact(精确匹配)、prefix(前缀匹配)、wildcard(通配符匹配)。
// bashPermissions.ts:870-934 — filterRulesByContentsMatchingInput
switch (bashRule.type) {
case 'exact': // 完全匹配命令
return bashRule.command === cmdToMatch
case 'prefix': // 前缀匹配 "git commit"
return cmdToMatch.startsWith(bashRule.prefix + ' ')
case 'wildcard': // 通配符匹配 "npm run *"
return matchWildcardPattern(bashRule.pattern, cmdToMatch)
}
5.2 规则优先级
在 bashToolCheckPermission() 中(bashPermissions.ts:1050-1178):
// 1. 精确匹配 deny → deny (最高优先级)
// 2. 精确匹配 ask → ask
// 3. 精确匹配 allow → allow
// 4. 前缀匹配 deny → deny
// 5. 前缀匹配 ask → ask
// 6. 路径约束检查 → ask/deny (如果路径越界)
// 7. 精确匹配 allow → allow
// 8. 前缀匹配 allow → allow
// 9. sed 约束检查 → ask (危险 sed 操作)
// 10. 模式检查 → allow (acceptEdits 模式)
// 11. 只读检查 → allow (白名单命令)
// 12. passthrough → 触发用户确认
5.3 环境变量剥离策略
系统对不同类型的规则使用不同的环境变量剥离策略:
// bashPermissions.ts:378-430 — SAFE_ENV_VARS
const SAFE_ENV_VARS = new Set([
'GOEXPERIMENT', 'GOOS', 'GOARCH', 'CGO_ENABLED', 'GO111MODULE',
'RUST_BACKTRACE', 'RUST_LOG',
'NODE_ENV',
'PYTHONUNBUFFERED', 'PYTHONDONTWRITEBYTECODE',
'LANG', 'LANGUAGE', 'LC_ALL', 'TERM', 'NO_COLOR', 'FORCE_COLOR',
// ... 安全的环境变量
])
// bashPermissions.ts:447-497 — ANT_ONLY_SAFE_ENV_VARS (内部员工专用)
const ANT_ONLY_SAFE_ENV_VARS = new Set([
'KUBECONFIG', 'DOCKER_HOST', // 注意:有安全风险,仅内部使用
'AWS_PROFILE', 'CLOUDSDK_CORE_PROJECT',
// ...
])
安全策略:
- allow 规则:只剥离
SAFE_ENV_VARS,防止DOCKER_HOST=evil docker ps自动匹配Bash(docker ps:*) - deny 规则:使用
stripAllLeadingEnvVars()剥离所有环境变量前缀,防止FOO=bar denied_command绕过拒绝规则
5.4 包装命令剥离
// bashPermissions.ts:532-560 — stripSafeWrappers
const SAFE_WRAPPER_PATTERNS = [
/^timeout[ \t]+(...复杂的 GNU 标志解析...)[ \t]+/,
/^time[ \t]+(?:--[ \t]+)?/,
/^nice(?:[ \t]+-n[ \t]+-?\d+|[ \t]+-\d+)?[ \t]+(?:--[ \t]+)?/,
/^stdbuf(?:[ \t]+-[ioe][LN0-9]+)+[ \t]+(?:--[ \t]+)?/,
/^nohup[ \t]+(?:--[ \t]+)?/,
]
这意味着 timeout 10 npm install foo 在匹配规则时会先剥离为 npm install foo。
6. 危险模式检测
6.1 破坏性命令警告(UI 层面)
destructiveCommandWarning.ts 检测已知危险模式但不影响权限逻辑,仅在 UI 显示警告:
const DESTRUCTIVE_PATTERNS = [
{ pattern: /\bgit\s+reset\s+--hard\b/, warning: '可能丢弃未提交的更改' },
{ pattern: /\bgit\s+push\b.*--force/, warning: '可能覆盖远程历史' },
{ pattern: /\bgit\s+clean\b.*-f/, warning: '可能永久删除未跟踪文件' },
{ pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*f/, warning: '可能递归强制删除' },
{ pattern: /\bDROP\s+TABLE\b/i, warning: '可能删除数据库表' },
{ pattern: /\bkubectl\s+delete\b/, warning: '可能删除 K8s 资源' },
{ pattern: /\bterraform\s+destroy\b/, warning: '可能销毁基础设施' },
]
6.2 路径危险检测(阻断级别)
pathValidation.ts:70-108 中的 checkDangerousRemovalPaths() 对 rm/rmdir 操作进行关键路径检查:
// 即使有 allow 规则,以下路径仍需用户确认:
// /, /usr, /bin, /sbin, /etc, /var, /home, /tmp, /opt, /lib
if (isDangerousRemovalPath(absolutePath)) {
return { behavior: 'ask', message: '危险的 rm 操作...' }
}
6.3 cd+git 组合攻击防护
// bashPermissions.ts:2202-2225
// 防止通过 cd 到恶意目录 + git status 触发 bare repo 的 core.fsmonitor RCE
if (compoundCommandHasCd) {
const hasGitCommand = subcommands.some(cmd => isNormalizedGitCommand(cmd))
if (hasGitCommand) {
return { behavior: 'ask', reason: 'cd + git 组合需要批准以防止 bare repository 攻击' }
}
}
6.4 只读命令白名单
readOnlyValidation.ts 维护了一个精细化的命令+标志白名单:
// COMMAND_ALLOWLIST 中的命令示例:
xargs: { safeFlags: { '-I': '{}', '-n': 'number', ... } }
git: { /* 大量 git 子命令+标志 */ }
grep: { safeFlags: { '-e': 'string', '-i': 'none', ... } }
sed: { safeFlags: { '-n': 'none', '-e': 'string', ... },
additionalCommandIsDangerousCallback: (cmd) => !sedCommandIsAllowedByAllowlist(cmd) }
每个白名单命令有一个 CommandConfig:
type CommandConfig = {
safeFlags: Record<string, FlagArgType> // 允许的标志
regex?: RegExp // 可选的额外验证
additionalCommandIsDangerousCallback?: (cmd, args) => boolean
respectsDoubleDash?: boolean // 是否尊重 POSIX --
}
7. 与更广泛权限系统的集成
7.1 LLM 分类器(Bash Classifier)
系统支持使用 LLM 对命令进行分类(bashClassifier.ts):
// bashPermissions.ts:1459-1481 — buildPendingClassifierCheck
// 在用户看到权限提示时,异步运行分类器
// 如果 LLM 高置信度允许 → 自动批准
export async function executeAsyncClassifierCheck(...) {
const classifierResult = await classifyBashCommand(command, cwd, descriptions, ...)
if (feature('BASH_CLASSIFIER') && classifierResult.matches && classifierResult.confidence === 'high') {
callbacks.onAllow({ type: 'classifier', reason: `Allowed by prompt rule: "..."` })
}
}
7.2 沙箱集成
// bashPermissions.ts:1270-1359 — checkSandboxAutoAllow
// 沙箱模式下的自动放行,但仍尊重 deny/ask 规则
if (SandboxManager.isSandboxingEnabled() && SandboxManager.isAutoAllowBashIfSandboxedEnabled()) {
// 检查显式 deny/ask 规则
// 无规则 → auto-allow (沙箱提供隔离)
}
7.3 模式系统
// modeValidation.ts:38-50
// acceptEdits 模式自动放行文件系统操作
if (toolPermissionContext.mode === 'acceptEdits' && isFilesystemCommand(baseCmd)) {
return { behavior: 'allow', decisionReason: { type: 'mode', mode: 'acceptEdits' } }
}
// 自动放行的命令: mkdir, touch, rm, rmdir, mv, cp, sed
7.4 子命令级别的安全上限
// bashPermissions.ts:103
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50
// 超过 50 个子命令 → ask(防止 ReDoS 和事件循环阻塞)
// bashPermissions.ts:110
export const MAX_SUGGESTED_RULES_FOR_COMPOUND = 5
// 复合命令最多建议 5 条规则
8. 关键代码片段
8.1 主入口:bashToolHasPermission()
// bashPermissions.ts:1663-2557
export async function bashToolHasPermission(
input: z.infer<typeof BashTool.inputSchema>,
context: ToolUseContext,
getCommandSubcommandPrefixFn = getCommandSubcommandPrefix,
): Promise<PermissionResult> {
// Step 0: AST parse
let astResult = await parseForSecurity(input.command)
// too-complex → ask (respect deny rules first)
if (astResult.kind === 'too-complex') {
const earlyExit = checkEarlyExitDeny(input, context)
if (earlyExit !== null) return earlyExit
return { behavior: 'ask', ... }
}
// simple → checkSemantics (eval, zsh builtins, etc.)
if (astResult.kind === 'simple') {
const sem = checkSemantics(astResult.commands)
if (!sem.ok) { /* deny/ask rules first, then ask */ }
}
// Steps 1-7: 精确匹配 → LLM → 管道 → 子命令拆分 → 最终决策
// ... (见第2节流程图)
}
8.2 stripSafeWrappers() 的安全设计
// bashPermissions.ts:524-615
export function stripSafeWrappers(command: string): string {
// Phase 1: 剥离环境变量(仅 SAFE_ENV_VARS)
// SECURITY: 使用 [ \t]+ 而非 \s+ —— \s 匹配 \n/\r(命令分隔符)
const ENV_VAR_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=([A-Za-z0-9_./:-]+)[ \t]+/
// Phase 2: 剥离包装命令(timeout, time, nice, nohup)
// 两阶段分离的原因:
// 包装命令后的 VAR=val 被视为要执行的命令,不是环境变量
// HackerOne #3543050
}
8.3 AST 语义检查:包装命令剥离
// ast.ts:2213-2384 — checkSemantics
// 递归剥离包装命令后检查实际命令名
for (;;) {
if (a[0] === 'time' || a[0] === 'nohup') { a = a.slice(1) }
else if (a[0] === 'timeout') { /* 解析 GNU 标志 + 时长 */ }
else if (a[0] === 'nice') { /* 解析 -n N / -N / bare */ }
else if (a[0] === 'env') { /* 解析 VAR=val + 安全标志 */ }
else if (a[0] === 'stdbuf') { /* 解析 -o0 / --output=M */ }
else { break }
}
// 然后检查实际命令名是否危险
const name = a[0]
// EVAL_LIKE_BUILTINS: eval, source, exec, trap, enable, hash
if (EVAL_LIKE_BUILTINS.has(name)) return { ok: false, ... }
8.4 路径提取器
// pathValidation.ts:190-509 — PATH_EXTRACTORS
// 每个命令有专门的参数解析逻辑
export const PATH_EXTRACTORS: Record<PathCommand, (args: string[]) => string[]> = {
cd: args => (args.length === 0 ? [homedir()] : [args.join(' ')]),
ls: args => { const p = filterOutFlags(args); return p.length > 0 ? p : ['.'] },
find: args => { /* 处理 -path, -newer 等带路径的标志 */ },
grep: args => parsePatternCommand(args, flagsWithArgs),
sed: args => { /* 处理 -e, -f, 脚本参数 */ },
git: args => { /* git diff --no-index 是特殊情况 */ },
// ...
}
8.5 sed 命令的专项安全
// sedValidation.ts:473-629 — containsDangerousOperations
// 即使通过白名单匹配,仍需检查以下危险操作:
// - w/W 命令(写文件)
// - e/E 命令(执行命令)
// - 非 ASCII 字符(Unicode 同形字攻击)
// - 花括号块(过于复杂)
// - 否定操作符 (!)
// - 波浪号步进地址 (~)
9. 关键安全特性总结
| 特性 | 实现位置 | 说明 |
|---|---|---|
| AST 解析 | ast.ts:381-460 | tree-sitter-bash, fail-closed |
| 语义检查 | ast.ts:2213-2399 | 包装命令剥离 + 危险内置命令检测 |
| 安全验证器链 | bashSecurity.ts:2308-2378 | 20+ 独立验证器 |
| 规则匹配 | bashPermissions.ts:778-935 | exact/prefix/wildcard, 固定点迭代 |
| 路径约束 | pathValidation.ts:1013-1109 | 允许目录检查, 重定向验证 |
| 只读白名单 | readOnlyValidation.ts:128-1137 | 40+ 命令的 flag 级白名单 |
| sed 安全 | sedValidation.ts:247-301 | 白名单 + 黑名单双重检查 |
| 破坏性警告 | destructiveCommandWarning.ts:12-89 | UI 提示,不影响权限 |
| 沙箱集成 | bashPermissions.ts:1270-1359 | 沙箱内自动放行(尊重显式规则) |
| LLM 分类器 | bashPermissions.ts:1459-1658 | 异步高置信度自动批准 |
| cd+git 防护 | bashPermissions.ts:2202-2225 | 防止 bare repo fsmonitor RCE |
| 环境变量策略 | bashPermissions.ts:378-497, 733-776 | allow 用白名单, deny 用全量剥离 |
| 子命令上限 | bashPermissions.ts:103 | 50 个,防止 ReDoS |
| 反斜杠操作符 | bashSecurity.ts:1629-1721 | 检测 \; | 等解析分歧 |
| 花括号展开 | bashSecurity.ts:1751-1892 | 深度追踪 + 引号花括号检测 |
| shell-quote bug | bashSecurity.ts:2277-2284 | 单引号内反斜杠的解析错误 |
| Unicode 空白 | ast.ts:262-263 | NBSP, 零宽空格等 |
| 控制字符 | ast.ts:254 | 0x00-0x08, 0x0B-0x1F, 0x7F |
-- 处理 | pathValidation.ts:126-139 | POSIX end-of-options 正确处理 |