VNX-RUST-004 – Rust Command Injection via process::Command
Overview
This rule detects Rust code that constructs a std::process::Command using the format! macro to build the command string, or that invokes a shell interpreter (sh -c) through Command. When user-controlled input is interpolated into the command string, an attacker can inject shell metacharacters (;, |, &&, $(...)) to execute arbitrary commands on the host system.
Severity: High | CWE: CWE-78 – Improper Neutralization of Special Elements used in an OS Command
Why This Matters
Command injection is consistently ranked among the most critical application vulnerabilities:
- Remote Code Execution (RCE): An attacker can run any command the application’s user can — read files, install backdoors, pivot to other systems
- Rust doesn’t protect you automatically: Unlike SQL parameterization, Rust’s type system doesn’t prevent you from building command strings unsafely
- Shell invocation amplifies risk: Passing arguments through
sh -cexposes the full shell metacharacter surface (pipes, redirections, command substitution) - Exploitation is trivial: If user input flows into a command string, the attacker simply includes
;malicious_commandin their input
What Gets Flagged
Pattern 1: Command::new with format! macro
// Flagged: user input interpolated into command string
let cmd = Command::new(format!("grep {} /var/log/app.log", user_query))
.output();
// Flagged: format! used to construct the program path
let output = Command::new(format!("/opt/tools/{}", tool_name))
.output()?;
Pattern 2: Shell invocation via sh -c
// Flagged: shell invocation with user input
let output = Command::new("sh").arg("-c")
.arg(format!("find /data -name '{}'", filename))
.output()?;
The rule applies only to .rs files.
Remediation
Pass user input as separate arguments, never as part of the command string.
Command::newwith.arg()does not invoke a shell and does not interpret metacharacters:use std::process::Command; // Safe: user_query is passed as a separate argument let output = Command::new("grep") .arg(&user_query) // Treated as a literal string, not shell-parsed .arg("/var/log/app.log") .output()?;Avoid shell invocation entirely. Do not use
sh -corbash -cunless absolutely necessary:// Instead of: Command::new("sh").arg("-c").arg(format!("wc -l {}", path)) // Use: let output = Command::new("wc") .arg("-l") .arg(&path) // Safe: no shell interpretation .output()?;Validate and sanitize input if it must form part of a command. Use an allowlist of acceptable characters:
fn is_safe_filename(name: &str) -> bool { name.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') } if !is_safe_filename(&user_input) { return Err(anyhow!("Invalid filename")); }Use Rust-native libraries instead of shelling out. For common operations, prefer crates:
// Instead of shelling out to `find`: use walkdir::WalkDir; for entry in WalkDir::new("/data").into_iter().filter_map(|e| e.ok()) { if entry.file_name().to_str() == Some(&target) { // found } }If you must use format! with Command, ensure the interpolated values are from trusted sources (constants, configuration files you control, enum variants) — never from user input, environment variables, or external APIs.