id: no-shell-format-arg
language: rust
severity: error
category: security
message: "Shell command line built with format!(). An interpolated value can inject shell syntax."
note: "The command runs through a shell (sh -c, cmd /C, powershell -Command), which parses the formatted string: quotes, ;, | or $( ) in the interpolated value become shell syntax. Without a shell, std::process::Command passes each .arg() to the program verbatim, so a formatted flag such as format!(\"--width={w}\") is safe and is not reported."
fix: "Call the program directly with Command::new(program) and one .arg() per value. If a shell is required, pass the value as a positional parameter (sh -c 'cmd \"$1\"' sh value) instead of interpolating it."
rule:
pattern: $CMD.arg(format!($$$ARGS))
inside:
kind: function_item
stopBy: end
all:
- has:
kind: call_expression
regex: 'Command::new\(\s*"(sh|bash|zsh|dash|ksh|cmd|cmd\.exe|powershell|pwsh)"'
stopBy: end
- has:
kind: string_literal
regex: '^"(-c|/C|/c|-Command)"$'
stopBy: end
ignores:
- "**/tests/**"
- "**/*_tests/**"
- "**/*_test/**"
- "**/test_*"
- "**/*_test.rs"
skip_test_code: true
tests:
should_match:
- |
fn run(user: &str) {
Command::new("sh").arg("-c").arg(format!("ls {}", user)).status();
}
- |
fn build(dir: &str) {
let mut cmd = std::process::Command::new("bash");
cmd.arg("-c");
cmd.arg(format!("cd {dir} && make"));
}
- |
fn win(path: &str) {
Command::new("cmd").arg("/C").arg(format!("type {path}"));
}
should_not_match:
- |
fn link(arg: &str) {
Command::new("rustc").arg("-C").arg(format!("link-arg={}", arg));
}
- |
fn width(cmd: &mut Command, w: u16) {
cmd.arg(format!("--diagnostic-width={w}"));
}
- |
fn script(root: &str) {
Command::new("sh").arg(format!("{root}/configure"));
}
- 'cmd.arg("--verbose");'
- 'let msg = format!("hi {}", name);'