Appearance
20|BashTool 安全:命令注入防护
上一篇的权限体系管住了大部分工具。但有一类工具特别难管:shell 命令。
read_log 是结构化的——参数就那几个,校验容易。shell 命令不是。一个 ; 就能拼接出任意命令,$(...) 能执行命令替换,反引号、管道、重定向都能改变命令语义。agent 执行 nginx -t 验证配置是合理的,但同样的入口让它执行 rm -rf / 也是可能的。本篇讲 BashTool 怎么设计才能安全。
shell 表达力太强是根本难点
BashTool 的复杂度全来自 shell 的表达力。
cat /var/log/nginx/error.log 看起来是个只读命令。但 cat /var/log/nginx/error.log; rm -rf /tmp/* 用分号拼接,第二条命令完全不同。cat $(cat /etc/passwd) 用命令替换,把文件内容当成命令执行。这些不是模型会主动构造的——是材料里混入的指令注入,或用户输入里的恶意构造。
字符串黑名单挡不住。在命令字符串里搜 rm、delete、drop 这些关键词,攻击者用大小写变换、编码绕过、命令替换就绕过了。RM、r""m、$(echo rm) 都能逃过简单的关键词搜索。可靠的做法是结构化解析,不是字符串匹配。
AST 解析复合命令
第一步是把命令字符串解析成语法树(AST),对每个子命令独立检查。分号、管道、&& 拼接的复合命令,拆成单个命令分别评估:

python
def parse_and_check(command: str) -> list[dict]:
# 用 shell 解析器把复合命令拆成子命令
subcommands = shell_parser.split(command)
results = []
for sub in subcommands:
results.append(check_single_command(sub))
return resultscat log; rm file 拆成 cat log 和 rm file 两条,分别检查。第一条通过(只读白名单),第二条拒绝(不在白名单)。不解析直接看整串,可能因为开头是合法的 cat log 就放行了,漏掉后面的 rm。
只读白名单看 flag 语义
只读判断不能只看命令名。cat 通常是只读,但 cat > file 是写操作(重定向写入)。白名单要看 flag 的具体语义和值类型:
python
READONLY_WHITELIST = {
"cat": {"flags": [], "no_redirect": True}, # 不能带重定向
"grep": {"flags": [], "no_redirect": True},
"ls": {"flags": ["-l", "-a"], "no_redirect": True},
"tail": {"flags": ["-n"], "no_redirect": True},
}
def is_readonly(command):
cmd = command.split()[0]
if cmd not in READONLY_WHITELIST:
return False
spec = READONLY_WHITELIST[cmd]
if spec.get("no_redirect") and has_redirect(command):
return False # cat > file 不是只读
if not all(flag_allowed(cmd, f, spec) for f in extract_flags(command)):
return False
return True只看命令名 cat 就放行,会漏掉 cat > /etc/passwd 这种写操作。flag 语义检查把 cat > 识别为写,拒绝。
注入检测
除了命令本身,还要检测注入风险。几类常见的:

命令替换 $(...) 和反引号:把任意命令的输出当成参数,可能执行非预期操作。进程替换 <(...):类似命令替换。参数替换:构造特殊参数绕过校验。控制字符和 Unicode 空白:用不可见字符混淆命令。
python
INJECTION_PATTERNS = [
r"\$\(", # 命令替换
r"`", # 反引号命令替换
r"<\(", # 进程替换
r"[\x00-\x1f]", # 控制字符
r"[-
]", # Unicode 零宽空白
]
def detect_injection(command):
for pattern in INJECTION_PATTERNS:
if re.search(pattern, command):
return True, pattern
return False, None这些模式出现在命令里,基本就是注入尝试。正常的运维命令不会用命令替换或零宽空白。
沙箱是安全网不是替代
沙箱能限制文件访问、网络连接、资源使用,是最后一层安全网。但沙箱不能替代权限判断。

python
# 沙箱配置示例
SANDBOX = {
"allowed_paths": ["/var/log/", "/tmp/ops-assistant/"],
"network": False,
"max_cpu_seconds": 30,
}沙箱能防的是"命令执行后的影响范围"——限制在允许路径内、禁网络、限 CPU。但它防不了所有攻击:CPU 密集型计算仍可能导致拒绝服务,沙箱内进程仍消耗资源。沙箱是安全网,不是唯一防线。显式的 deny/ask 规则优先于沙箱,沙箱只在规则放行后兜底。
多层检查的顺序
BashTool 的安全检查是一套多层管线,顺序不能乱:
- AST 解析复合命令,拆成子命令
- 每个子命令做注入检测(命令替换、控制字符等)
- 只读白名单检查(命令名 + flag 语义 + 重定向)
- 路径检查(是否访问敏感路径)
- 权限管线判断(第 19 篇的六种模式)
- 沙箱执行
python
def execute_bash(command, context, mode):
subcommands = parse_and_check(command)
for sub in subcommands:
if detect_injection(sub["raw"])[0]:
return {"ok": False, "error": "检测到注入模式"}
if not is_readonly(sub) and not check_write_permission(sub, mode):
return {"ok": False, "error": "写操作未授权"}
if is_sensitive_path(sub):
return {"ok": False, "error": "敏感路径"}
# 全部通过,沙箱执行
return run_in_sandbox(command, SANDBOX)合法命令被误拦截时,按这八层排查:AST 解析是否拆错、白名单是否漏了合法命令、注入检测是否过敏、路径检查是否拒了合法路径。逐层查比盲目放宽策略安全。
BashTool 之后
BashTool 是最复杂的工具,专门用一篇讲。但运维排查里大部分工具不是 shell 命令,而是结构化的只读和写操作工具——日志查询、指标读取、配置变更、文件编辑。这些工具的设计原则和 BashTool 不同,更结构化、更可控。
下一篇讲只读与写操作工具的分级设计。