Command Injection in Rocket
How Command Injection Manifests in Rocket
Command injection vulnerabilities in Rocket applications occur when user-controlled input is passed directly to system commands without proper sanitization. In Rocket, this typically happens in route handlers that execute shell commands or invoke external processes based on request parameters.
The most common pattern involves using Rust's std::process::Command or std::process::Command::new to execute system commands where user input becomes part of the command arguments. For example:
#[get("/execute?")]
fn execute_command(command: String) -> String {
let output = Command::new("/bin/bash")
.arg("-c")
.arg(command)
.output()
.expect("Failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}
This handler is critically vulnerable because any user can inject arbitrary commands. A malicious request like /execute?command=echo+%22hello%22+%26%26+cat+/etc/passwd would execute both commands sequentially.
Another common Rocket-specific pattern involves using the Command::args method with user-controlled vectors:
#[post("/run", data = "<'r> json")]
fn run_command(json: Json<CommandRequest>) -> String {
let mut cmd = Command::new(json.command.clone());
// Directly using user-provided arguments without validation
for arg in json.args.clone() {
cmd.arg(arg);
}
let output = cmd.output().expect("Command failed");
String::from_utf8_lossy(&output.stdout).to_string()
}
Attackers can exploit this by crafting arguments that break out of the intended command structure. For instance, if the backend expects arguments like ['ls', '-l', '/tmp'], an attacker might send ['ls', '-l', '/tmp', ';', 'cat', '/etc/passwd'] to execute additional commands.
Rocket's request guards can also introduce vulnerabilities when they're used to capture complex data structures that include command parameters:
#[derive(Deserialize)]
struct ProcessRequest {
program: String,
arguments: Vec<String>,
flags: HashMap<String, String>,
}
#[post("/process")]
fn process(req: Json<ProcessRequest>) -> String {
let mut cmd = Command::new(req.program.clone());
// User controls both keys and values in flags
for (key, value) in req.flags.clone() {
cmd.arg(format!("--{}={}", key, value));
}
let output = cmd.output().expect("Failed");
String::from_utf8_lossy(&output.stdout).to_string()
}
This pattern is particularly dangerous because attackers can manipulate both flag names and values to construct malicious command sequences.
Rocket-Specific Detection
Detecting command injection in Rocket applications requires both static code analysis and dynamic runtime scanning. Static analysis should focus on identifying patterns where user input flows into system command execution.
Using middleBrick's CLI to scan a Rocket API endpoint:
npx middlebrick scan https://api.example.com/rocket-api
The scanner examines the unauthenticated attack surface and identifies endpoints that accept parameters which could be used for command injection. For Rocket applications, middleBrick specifically looks for:
- GET parameters that might be passed to shell commands
- POST request bodies containing command-related fields
- Request guards that deserialize into command execution structures
- Endpoints with names suggesting system interaction (execute, run, cmd, shell, etc.)
middleBrick's dynamic scanning tests these endpoints with various injection payloads:
# Basic command chaining
payload1 = "valid_command && whoami"
# Output redirection
payload2 = "echo 'test' > /tmp/testfile"
# Subshell execution
payload3 = "$(cat /etc/passwd)"
# Semicolon injection
payload4 = "command; rm -rf /tmp/test"
The scanner evaluates responses for indicators of successful command execution, such as:
- Unexpected output in response bodies
- Changes in response timing (indicating command execution)
- HTTP status codes that suggest command failures
- Changes in server state observable through subsequent requests
middleBrick also analyzes the OpenAPI/Swagger specification if available, mapping parameter definitions to runtime behavior and identifying mismatches between documented and actual functionality.
For CI/CD integration, you can add Rocket API security checks to your pipeline:
name: Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan Rocket API
run: |
npx middlebrick scan https://staging.example.com/api
continue-on-error: true
- name: Fail on critical issues
run: |
if [ $(npx middlebrick status | grep -c "Critical") -gt 0 ]; then exit 1; fi
This setup ensures that any command injection vulnerabilities in your Rocket endpoints are caught before deployment.
Rocket-Specific Remediation
Remediating command injection in Rocket requires eliminating unsafe command execution patterns and implementing proper input validation. The most secure approach is to avoid shell command execution entirely when possible.
Instead of using shell commands, use Rust's standard library or crates that provide safe abstractions:
// Unsafe - vulnerable to injection
let output = Command::new("/bin/bash")
.arg("-c")
.arg(user_input)
.output();
// Safe - use dedicated crates
use std::fs;
use std::path::Path;
// For file operations
let content = fs::read_to_string(Path::new(&user_input_path))
.map_err(|e| format!("Read error: {}", e));
When command execution is unavoidable, use argument vectors instead of shell interpretation:
// ✅ Safe - no shell interpretation
let output = Command::new("ls")
.args(&["-l", "/tmp"])
.output();
// ❌ Dangerous - shell interpretation enabled
let output = Command::new("/bin/sh")
.arg("-c")
.arg("ls -l /tmp")
.output();
Implement strict input validation using Rocket's request guards for complex scenarios:
use rocket::request::FromForm;
use serde::Deserialize;
use std::collections::HashSet;
#[derive(Deserialize, Debug)]
struct SafeCommandRequest {
program: String,
arguments: Vec<String>,
}
#[derive(Debug)]
struct ValidatedCommand(SafeCommandRequest);
impl<'a> rocket::request::FromForm<'a> for ValidatedCommand {
type Error = String;
fn from_form(items: &mut rocket::request::FormItems<'a>, _strict: bool) -> Result<Self, Self::Error> {
let req: SafeCommandRequest = rocket::request::Form::from_form(items, false)?.into_inner();
// Whitelist allowed programs
let allowed_programs: HashSet<&str> = ["ls", "cat", "grep"].iter().cloned().collect();
if !allowed_programs.contains(req.program.as_str()) {
return Err("Program not allowed".into());
}
// Validate arguments against patterns
for arg in &req.arguments {
if arg.contains(&[';', '&', '|', '$', '`'][..]) {
return Err("Invalid characters in arguments".into());
}
}
Ok(ValidatedCommand(req))
}
}
#[post("/safe-execute", data = "<'r> json")]
fn safe_execute(req: Json<ValidatedCommand>) -> String {
let SafeCommandRequest { program, arguments } = req.0;
let mut cmd = Command::new(program);
cmd.args(arguments);
let output = cmd.output().expect("Command failed");
String::from_utf8_lossy(&output.stdout).to_string()
}
For file system operations that might be alternatives to command execution:
use rocket::tokio::fs;
#[get("/read-file?")]
async fn read_file(path: String) -> Result<String, String> {
// Resolve to absolute path and validate it's within allowed directory
let base_dir = std::env::current_dir().unwrap();
let target_path = base_dir.join(path);
// Ensure path is within base directory
if !target_path.starts_with(&base_dir) {
return Err("Path traversal attempt detected".into());
}
// Check file exists and is readable
if !target_path.exists() {
return Err("File not found".into());
}
let content = fs::read_to_string(target_path)
.await
.map_err(|e| format!("Read error: {}", e))?;
Ok(content)
}
Using Rocket's built-in validation features with serde for structured input:
use rocket::request::FromForm;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct ProcessConfig {
#[serde(deserialize_with = "validate_program")] // Custom validation
program: String,
#[serde(deserialize_with = "validate_args")] // Custom validation
arguments: Vec<String>,
}
fn validate_program<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let allowed = ["ls", "cat", "wc"];
if !allowed.contains(&s.as_str()) {
return Err(serde::de::Error::custom("Program not in whitelist"));
}
Ok(s)
}
fn validate_args<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let args = Vec::deserialize(deserializer)?;
for arg in &args {
if arg.contains(&[';', '&', '|', '$', '`'][..]) {
return Err(serde::de::Error::custom("Invalid character in argument"));
}
}
Ok(args)
}
Continuous monitoring with middleBrick Pro can help maintain security posture:
# Continuous monitoring configuration
middlebrick monitor https://api.example.com \
--schedule=daily \
--threshold=70 \
--alert=slack \
--webhook=https://hooks.slack.com/services/...
This setup scans your Rocket API endpoints regularly and alerts your team if security scores drop below acceptable thresholds, ensuring command injection vulnerabilities are caught early.
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 |