Skip to content

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) 用命令替换,把文件内容当成命令执行。这些不是模型会主动构造的——是材料里混入的指令注入,或用户输入里的恶意构造。

字符串黑名单挡不住。在命令字符串里搜 rmdeletedrop 这些关键词,攻击者用大小写变换、编码绕过、命令替换就绕过了。RMr""m$(echo rm) 都能逃过简单的关键词搜索。可靠的做法是结构化解析,不是字符串匹配。

AST 解析复合命令

第一步是把命令字符串解析成语法树(AST),对每个子命令独立检查。分号、管道、&& 拼接的复合命令,拆成单个命令分别评估:

AIOps 概念图:Shell 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 results

cat log; rm file 拆成 cat logrm 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 > 识别为写,拒绝。

注入检测

除了命令本身,还要检测注入风险。几类常见的:

AIOps 概念图:命令注入检测

命令替换 $(...) 和反引号:把任意命令的输出当成参数,可能执行非预期操作。进程替换 <(...):类似命令替换。参数替换:构造特殊参数绕过校验。控制字符和 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

这些模式出现在命令里,基本就是注入尝试。正常的运维命令不会用命令替换或零宽空白。

沙箱是安全网不是替代

沙箱能限制文件访问、网络连接、资源使用,是最后一层安全网。但沙箱不能替代权限判断。

AIOps 概念图:沙箱兜底

python
# 沙箱配置示例
SANDBOX = {
    "allowed_paths": ["/var/log/", "/tmp/ops-assistant/"],
    "network": False,
    "max_cpu_seconds": 30,
}

沙箱能防的是"命令执行后的影响范围"——限制在允许路径内、禁网络、限 CPU。但它防不了所有攻击:CPU 密集型计算仍可能导致拒绝服务,沙箱内进程仍消耗资源。沙箱是安全网,不是唯一防线。显式的 deny/ask 规则优先于沙箱,沙箱只在规则放行后兜底。

多层检查的顺序

BashTool 的安全检查是一套多层管线,顺序不能乱:

  1. AST 解析复合命令,拆成子命令
  2. 每个子命令做注入检测(命令替换、控制字符等)
  3. 只读白名单检查(命令名 + flag 语义 + 重定向)
  4. 路径检查(是否访问敏感路径)
  5. 权限管线判断(第 19 篇的六种模式)
  6. 沙箱执行
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 不同,更结构化、更可控。

下一篇讲只读与写操作工具的分级设计。