Command Injection in Axum with Mutual Tls
Command Injection in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
Command Injection occurs when an attacker can inject and execute arbitrary system commands through an application. In Axum, this often arises when user-controlled input is passed to a shell or a command executor without proper validation or sanitization. When Mutual Transport Layer Security (Mutual Tls) is used, the focus shifts to how identity and authorization are enforced at the application layer after the TLS handshake completes. While Mutual Tls ensures that both client and server present valid certificates, it does not inherently protect against malicious payloads embedded within authenticated requests.
Consider a scenario where an authenticated client presents a certificate, and the server extracts a field such as a Common Name (CN) or a custom certificate extension to use in constructing system commands. If this data is concatenated into a shell command without escaping, an attacker with a valid certificate could inject shell metacharacters. For example, a CN value like test; cat /etc/passwd could lead to unintended command execution. This is especially dangerous in Axum handlers that invoke external utilities for administrative or diagnostic tasks, as the trust boundary is incorrectly assumed to be at the TLS layer rather than at the input validation layer.
Even with Mutual Tls, Axum applications often rely on runtime parameters, headers, or path segments that may be derived from certificate attributes. If those attributes are not treated as untrusted input, the combination of Mutual Tls and dynamic command construction creates a blind spot. Security scans using tools that test the unauthenticated attack surface can detect such command injection vectors by probing endpoints that execute shell commands, regardless of the TLS configuration. The key takeaway is that Mutual Tls provides channel integrity and peer authentication, but it does not sanitize data that flows through the application logic.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
Remediation focuses on strict input validation, avoiding shell invocation, and ensuring that certificate-derived data is never directly concatenated into commands. Below are concrete Axum examples demonstrating secure patterns.
1. Avoid shell execution entirely by using structured commands
Instead of invoking a shell, use Rust's std::process::Command with explicit arguments. This prevents the shell from interpreting metacharacters.
use axum::{routing::get, Router};
use std::process::Command;
async fn handle_user_info(username: String) -> String {
// Safe: pass arguments directly without shell
let output = Command::new("id")
.arg(&username)
.output()
.expect("failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}
fn app() -> Router {
Router::new()
.route("/user/:username", get(|username: String| async move {
handle_user_info(username).await
}))
}
2. Validate and sanitize certificate-derived inputs
If you must use data from client certificates (e.g., via request extensions), treat it as untrusted. Use allowlists and reject any input containing shell metacharacters.
use axum::{routing::post, Extension, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CommandRequest {
action: String,
}
fn is_valid_input(s: &str) -> bool {
// Allow only alphanumeric and underscores
s.chars().all(|c| c.is_alphanumeric() || c == '_')
}
async fn run_action(Json(payload): Json) -> String {
if !is_valid_input(&payload.action) {
return String::from("invalid input");
}
// Safe execution with validated input
let output = std::process::Command::new("logger")
.arg(&payload.action)
.output()
.unwrap();
format!("logged: {}", String::from_utf8_lossy(&output.stdout))
}
fn app() -> axum::Router {
axum::Router::new()
.route("/run", post(run_action))
}
3. Enforce Mutual Tls and inspect certificate fields safely
When using tower-rs or similar middleware to enforce Mutual Tls, extract certificate fields and validate them before any use. Do not pass them directly to shell commands.
use axum::{async_trait, extract::Request, middleware, Router};
use std::convert::Infallible;
use tls_parser::Certificate;
struct CertValidator;
#[async_trait]
impl tower::Layer for CertValidator {
type Service = ValidatedService;
fn layer(&self, inner: S) -> Self::Service {
ValidatedService { inner }
}
}
struct ValidatedService {
inner: S,
}
impl tower::Service> for ValidatedService
where
S: tower::Service, Response = axum::response::Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send + 'static,
B: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = std::future::Ready>;
fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, mut req: Request) -> Self::Future {
// Example: inspect certificate extension for allowed values
if let Some(cert) = req.extensions().get::() {
let subject = cert.subject();
let cn = subject.common_name().unwrap_or_default();
if cn.contains(';') || cn.contains('&') || cn.contains('|') {
let response = axum::response::Response::builder()
.status(403)
.body("Forbidden".into())
.unwrap();
return std::future::ready(Ok(response));
}
}
let fut = self.inner.call(req);
std::future::ready(fut.map(|res| res.map_err(|err| err.into())))
}
}
fn app() -> Router {
Router::new()
.route_safe("/admin", get(|| async { "ok" }))
.layer(CertValidator)
} 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 |