Command Injection in Actix (Rust)
Command Injection in Actix with Rust — how this specific combination creates or exposes the vulnerability
Command injection in an Actix web service written in Rust typically arises when user-controlled input is passed to a system command without validation or sanitization. Actix is a powerful, actor-based Rust framework; while it does not inherently introduce command injection, its routing and handler design can inadvertently expose dangerous patterns when integrating with external processes. A common scenario involves spawning a subprocess from an HTTP handler using std::process::Command and concatenating user input directly into arguments.
For example, consider an endpoint that accepts a filename query parameter to generate a report via an external utility. If the handler builds the command by string concatenation, an attacker can supply additional shell metacharacters or separate commands to execute arbitrary code on the host. Because Actix handlers are asynchronous and often run in multi-threaded Tokio runtimes, the impact of a successful injection can be severe, potentially leading to remote code execution, data exfiltration, or lateral movement within the environment.
Another vector involves environment variables or configuration values that are dynamically interpolated into commands. An attacker who can influence these inputs through compromised endpoints or insecure deployment pipelines may leverage them to alter command behavior. Even when using Actix’s form extractors or JSON payloads, failing to validate and sanitize values before passing them to shell utilities creates a path for injection. The risk is compounded when the service runs with elevated privileges or has access to sensitive resources, as is typical in microservice architectures that rely on external tooling for file conversion, compression, or system diagnostics.
middleBrick detects such patterns by analyzing the unauthenticated attack surface of Actix APIs, identifying endpoints that accept free-form input and inspecting how that input may flow into subprocess creation. Although the scanner does not execute exploits, it highlights risky argument construction and missing input constraints, enabling developers to apply Rust-specific mitigations. Understanding the interaction between Actix’s routing model and Rust’s process spawning mechanisms is essential for building secure handlers that avoid command injection while maintaining functionality.
Rust-Specific Remediation in Actix — concrete code fixes
To prevent command injection in Actix with Rust, always avoid passing user-controlled data directly to shell commands. Prefer using std::process::Command with explicit arguments rather than shell interpretation. Validate and sanitize inputs rigorously, and consider using allowlists for expected values. Below are concrete, safe patterns for common use cases.
- Safe argument construction without a shell:
use actix_web::{web, HttpResponse};
use std::process::Command;
async fn generate_report(path: web::Query<HashMap<String, String>>) -> HttpResponse {
let filename = match path.get("filename") {
Some(name) if name.chars().all(|c| c.is_alphanumeric() || c == '.' || c == '_') => name,
_ => return HttpResponse::BadRequest().body("Invalid filename"),
};
// Do not use shell; pass arguments directly
let output = Command::new("/usr/bin/reportgen")
.arg("--output")
.arg(format!("/tmp/{}", filename))
.output();
match output {
Ok(o) if o.status.success() => HttpResponse::Ok().body(String::from_utf8_lossy(&o.stdout).into_owned()),
_ => HttpResponse::InternalServerError().body("Report generation failed"),
}
}
- Using a controlled helper binary with serialized input:
use actix_web::post;
use serde::Deserialize;
use std::process::Command;
#[derive(Deserialize)]
struct ExportRequest {
template_id: u32,
}
#[post("/export")]
async fn export_data(req_body: web::Json<ExportRequest>) -> actix_web::Result<HttpResponse> {
// Validate template_id against an allowlist or database
if !(1..=100).contains(&req_body.template_id) {
return Ok(HttpResponse::BadRequest().body("Invalid template"));
}
// Spawn a dedicated binary; no shell involved
let status = Command::new("/opt/export-worker")
.arg(&req_body.template_id.to_string())
.status()
.map_err(|_| actix_web::error::ErrorInternalServerError("Worker failed to start"))?;
if status.success() {
Ok(HttpResponse::Ok().finish())
} else {
Ok(HttpResponse::ServiceUnavailable().finish())
}
}
- Environment variable handling without interpolation:
use actix_web::get;
use std::process::Command;
#[get("/health")]
async fn health_check() -> actix_web::Result<HttpResponse> {
let helper_path = std::env::var("REPORTGEN_PATH").unwrap_or_else(|_| "/usr/bin/reportgen".into());
// Use the path as a single argument; do not invoke via shell
let out = Command::new(helper_path)
.arg("--check")
.output();
match out {
Ok(o) if o.status.success() => Ok(HttpResponse::Ok().finish()),
_ => Ok(HttpResponse::ServiceUnavailable().finish()),
}
}
These patterns ensure that user input never reaches a shell, eliminating classic command injection vectors. When integrating with external tooling, prefer explicit argument lists and strict allowlists. middleBrick’s scans can highlight endpoints where user data reaches subprocess creation, helping teams focus remediation on the most dangerous handlers.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |