Command Injection in Axum
How Command Injection Manifests in Axum
Command injection in Axum applications typically occurs when user input is passed directly to system commands without proper sanitization. This vulnerability is particularly dangerous because it allows attackers to execute arbitrary commands on the server, potentially leading to data exfiltration, system compromise, or lateral movement within the network.
In Axum applications, command injection often appears in routes that handle file operations, system utilities, or external tool integration. For example, a route that executes shell commands to process user-uploaded files or validate content might inadvertently expose the system to injection attacks.
use axum::routing::get;
use axum::Router;
use tokio::process::Command;
async fn process_file(name: String) -> String {
// VULNERABLE: Direct interpolation of user input into shell command
let output = Command::new("sh")
.arg("-c")
.arg(format!("file_processor {} 2>&1", name))
.output()
.await
.expect("Failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}
let app = Router::new()
.route("/process/:name", get(process_file));
The above code demonstrates a classic command injection vulnerability. An attacker could craft a filename like legitimate.txt; rm -rf / to execute destructive commands. The issue stems from using sh -c with string interpolation, which allows shell metacharacters to be interpreted.
Another common pattern in Axum applications involves using environment variables or configuration values that originate from user input. If these values are used in command execution without validation, they create injection vectors:
use axum::routing::get;
use axum::Router;
use std::process::Command;
async fn run_analysis(tool: String) -> String {
// VULNERABLE: Tool name from user input used directly
let output = Command::new(tool)
.arg("--analyze")
.arg("/data/input.txt")
.output()
.expect("Failed to execute tool");
String::from_utf8_lossy(&output.stdout).to_string()
}
In this case, an attacker could specify a tool like bash and potentially inject commands through the arguments. The vulnerability is compounded when combined with path traversal or other input manipulation techniques.
Axum-Specific Detection
Detecting command injection in Axum applications requires both static analysis of the codebase and dynamic testing of the running application. Static analysis can identify risky patterns like direct use of Command::new() with interpolated strings or the use of sh -c with user input.
middleBrick's API security scanner can detect command injection vulnerabilities in Axum applications by analyzing the runtime behavior of API endpoints. The scanner tests for command injection by sending specially crafted payloads that attempt to execute harmless test commands and observing the responses. For example, it might send a payload containing echo VULNERABLE and check if this appears in the response.
The scanner's input validation check specifically looks for endpoints that might be constructing shell commands from user input. It analyzes the request parameters, headers, and body to identify potential injection points. The tool also checks for the presence of system command execution patterns in the application's response times and error messages.
For Axum applications specifically, middleBrick examines the routing structure to identify endpoints that handle file operations, system utilities, or external tool integration. These endpoints are prioritized for command injection testing because they're most likely to contain vulnerable code.
During scanning, middleBrick tests various injection payloads including:
- Simple command concatenation (
|| ls,&& whoami) - Redirect operators (
; cat /etc/passwd) - Command substitution (
$(id),`ls`) - Environment variable manipulation
- Path traversal combined with command execution
The scanner also checks for proper error handling, as applications that expose detailed error messages can provide attackers with valuable information about the underlying system and potential injection points.
Axum-Specific Remediation
Remediating command injection in Axum applications requires a defense-in-depth approach. The primary strategy is to avoid shell command execution entirely when possible, but when system commands are necessary, they must be executed safely.
The safest approach is to use dedicated libraries for file operations and system tasks rather than shell commands. For example, instead of using ls or find commands, use Rust's standard library or crates like walkdir:
use axum::routing::get;
use axum::Router;
use std::fs;
use std::path::Path;
async fn list_files(dir: String) -> Result {
let base_path = "/safe/base/path/";
let full_path = base_path.to_string() + &dir;
// Validate path to prevent directory traversal
if !full_path.starts_with(base_path) {
return Err("Invalid path".into());
}
if !Path::new(&full_path).exists() {
return Err("Directory not found".into());
}
// Use Rust's filesystem APIs instead of shell commands
let entries = match fs::read_dir(full_path) {
Ok(read_dir) => read_dir,
Err(_) => return Err("Cannot read directory".into()),
};
let mut result = String::new();
for entry in entries {
if let Ok(entry) = entry {
result.push_str(&format!("{}\n", entry.file_name().to_string_lossy()));
}
}
Ok(result)
}
When shell commands are unavoidable, use argument vectors instead of shell strings to prevent metacharacter interpretation:
use axum::routing::get;
use axum::Router;
use tokio::process::Command;
async fn process_image(file: String) -> String {
// VALID: Use argument vector, no shell interpretation
let output = Command::new("convert")
.arg(file)
.arg("output.png")
.output()
.await
.expect("Failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}
Implement strict input validation and sanitization. Validate file names against a whitelist of allowed characters, enforce length limits, and check for path traversal attempts:
use axum::routing::get;
use axum::Router;
use tokio::process::Command;
fn is_valid_filename(name: &str) -> bool {
// Allow only alphanumeric, hyphen, underscore, and dot
name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') &&
!name.contains("..") && // Prevent path traversal
name.len() <= 255
}
async fn safe_command(file: String) -> Result {
if !is_valid_filename(&file) {
return Err("Invalid filename".into());
}
let output = Command::new("file")
.arg("--brief")
.arg(file)
.output()
.await
.map_err(|_| "Command execution failed")?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
Consider using the cargo fuzz tool to create property-based tests that attempt to find command injection vulnerabilities through automated input generation. This can help identify edge cases that manual testing might miss.
For applications that must execute arbitrary commands, implement a command whitelist and use the exec family of functions with explicit argument lists rather than shell invocation. Always run commands with the least privilege necessary and consider using containers or sandboxes for particularly risky operations.
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 |
Frequently Asked Questions
How can I test my Axum application for command injection vulnerabilities?
;, &&, ||, and $() to endpoints that handle file names or system commands. Monitor the responses for signs of command execution, such as unexpected output or timing changes.Is using <code>std::process::Command</code> always safe in Axum?
std::process::Command is only safe when used correctly. The key is to avoid shell interpretation by using the argument vector form (passing arguments individually) rather than shell strings. Never use sh -c with interpolated user input. Even with argument vectors, you must still validate and sanitize all user input to prevent path traversal and other attacks.