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 -c exposes 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_command in their input

What Gets Flagged

A shell given a non-literal command string (the finding is on the Command::new line, whether the builder is chained on one line or split across lines):

Command::new("sh").arg("-c").arg(format!("ping -c 1 {}", host)).output()        // FLAGGED

let line = format!("convert {} {}", input, output);
let _ = Command::new("sh").args(["-c", &line]).output();                       // FLAGGED

let _ = Command::new("cmd").args(&["/C", &format!("copy {} backup\\", src)]);   // FLAGGED

Command::new("powershell.exe")                                                 // FLAGGED
    .args(["-NoProfile", "-Command", script])

let child = tokio::process::Command::new("sh")                                 // FLAGGED
    .arg("-c")
    .arg(&script)
    .spawn();

A program taken from the request:

pub async fn run_tool(Query(params): Query<Tool>) -> String {
    let out = Command::new(&params.name).output().unwrap();                    // FLAGGED

Not flagged: Command::new("sh").arg("-c").arg("sync") or .arg(BOOTSTRAP) (a constant), a value quoted with shlex::try_quote or shell_escape::escape before it is interpolated, Command::new("sh").arg("deploy.sh").arg(env) (a script file with arguments), and Command::new("git").arg("checkout").arg(branch) (no shell parses the argument).

Remediation

  1. Pass user input as separate arguments, never as part of the command string. Command::new with .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()?;
    
  2. Avoid shell invocation entirely. Do not use sh -c or bash -c unless 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()?;
    
  3. 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"));
    }
    
  4. 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
        }
    }
    
  5. 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.

References