Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • BashTool 权限系统深度解析

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-460tree-sitter-bash, fail-closed
语义检查ast.ts:2213-2399包装命令剥离 + 危险内置命令检测
安全验证器链bashSecurity.ts:2308-237820+ 独立验证器
规则匹配bashPermissions.ts:778-935exact/prefix/wildcard, 固定点迭代
路径约束pathValidation.ts:1013-1109允许目录检查, 重定向验证
只读白名单readOnlyValidation.ts:128-113740+ 命令的 flag 级白名单
sed 安全sedValidation.ts:247-301白名单 + 黑名单双重检查
破坏性警告destructiveCommandWarning.ts:12-89UI 提示,不影响权限
沙箱集成bashPermissions.ts:1270-1359沙箱内自动放行(尊重显式规则)
LLM 分类器bashPermissions.ts:1459-1658异步高置信度自动批准
cd+git 防护bashPermissions.ts:2202-2225防止 bare repo fsmonitor RCE
环境变量策略bashPermissions.ts:378-497, 733-776allow 用白名单, deny 用全量剥离
子命令上限bashPermissions.ts:10350 个,防止 ReDoS
反斜杠操作符bashSecurity.ts:1629-1721检测 \; | 等解析分歧
花括号展开bashSecurity.ts:1751-1892深度追踪 + 引号花括号检测
shell-quote bugbashSecurity.ts:2277-2284单引号内反斜杠的解析错误
Unicode 空白ast.ts:262-263NBSP, 零宽空格等
控制字符ast.ts:2540x00-0x08, 0x0B-0x1F, 0x7F
-- 处理pathValidation.ts:126-139POSIX end-of-options 正确处理