logo

Database

Need

Prevention of OS command injection by avoiding shells and validating program arguments

Context

• Usage of Rust 1.75+ for building application services

• Usage of the actix-web crate for handling HTTP requests

• Usage of std::process::Command for running external programs

Description

1. Non compliant code

use actix_web::{get, web, HttpResponse};
use serde::Deserialize;
use std::process::Command;

#[derive(Deserialize)]
struct PingQuery {
    host: String,
}...

The `ping` handler below inserts the `host` query parameter into a command line with `format!` and runs it through `sh -c`. The shell parses the whole string, so metacharacters in the parameter start new commands. A request to `/ping?host=127.0.0.1;cat /etc/passwd` runs `cat` with the privileges of the service, and payloads with `$(...)`, backticks, `|` or `&&` work the same way. The output of the injected command is returned in the response. Removing the shell is not enough when the value is passed as an argument without validation. `Command::new("git").arg("clone").arg(repo)` still lets a caller send `--upload-pack=touch /tmp/pwned`, which `git` interprets as an option that runs a program. This is argument injection, and it affects any tool whose options can execute code or write files.

2. Steps

• Do not run `sh -c`, `bash -c` or `cmd /C` with strings that contain request data; call the target program directly with `Command::new`.

• Pass each value with its own `.arg` call instead of building a single command string.

• Validate every request-derived argument against a strict format, such as parsing it into `IpAddr` or matching an allowlist, before it reaches the command.

• Reject values that start with `-`, and place user input after a `--` separator when the program supports it, to prevent argument injection.

• Prefer a Rust library over an external program when one exists, for example `git2` instead of running `git`.

• Run the service with an operating system user that has only the permissions it needs.

3. Secure code example

use actix_web::{get, web, HttpResponse};
use serde::Deserialize;
use std::net::IpAddr;
use std::process::Command;

#[derive(Deserialize)]
struct PingQuery {
    host: String,...

The corrected handler runs `ping` directly, without a shell, and passes each argument with a separate `.arg` call. `std::process::Command` hands those arguments to the program as they are, so characters such as `;`, `|` or `$(` have no special meaning. Before that, the `host` parameter is parsed as a `std::net::IpAddr`. Only a valid IPv4 or IPv6 address passes, which rejects shell metacharacters and also values that start with `-`, so the caller cannot inject options such as `-f` into `ping`. The command receives the normalized address produced by `IpAddr`, not the raw request string. When a program must receive free-form input, the same pattern applies: validate the value against a strict format, and place it after a `--` separator so the program never parses it as an option.