Formula Injection in Axum with Mutual Tls
Formula Injection in Axum with Mutual Tls
Formula Injection occurs when user-controlled data is interpreted as code, configuration, or query language within a downstream system. In Axum, a Rust web framework, this typically surfaces when values extracted from mutual TLS (mTLS) handshakes—such as certificate fields—are passed into system commands, queries, or template engines without validation. Mutual TLS adds client certificate verification, which can expose additional identity data (e.g., Common Name, Organization, or custom extensions) that developers may mistakenly trust. Because mTLS certificates often carry structured metadata, treating these fields as safe inputs can lead to command injection, template injection, or query manipulation if the data is not rigorously sanitized.
Consider an Axum handler that reads the Common Name (CN) from a client certificate and uses it directly in a shell command to look up user permissions:
use axum::{routing::get, Router};
use axum::extract::ConnectInfo;
use std::net::SocketAddr;
use axum::http::header::HeaderName;
async fn handler(ConnectInfo(info): ConnectInfo<SslInfo>) -> String {
let cn = info.peer_certificate
.and_then(|cert| extract_common_name(&cert))
.unwrap_or_else(|| "unknown".to_string());
// Risky: CN used in a shell command
let output = std::process::Command::new("get_permissions")
.arg("--user")
.arg(cn)
.output()
.expect("failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}
If the CN contains shell metacharacters (e.g., '; rm -rf / #), the command execution can be hijacked. This is Formula Injection because the attacker-controlled certificate field alters the intended behavior of the command. Axum does not sanitize mTLS-derived inputs by design; it is the developer’s responsibility to treat certificate data as untrusted. The same risk applies when using certificate fields in SQL queries or configuration templates, where an attacker could inject malicious clauses or paths.
In the context of mTLS, formula injection is amplified because certificate metadata is often assumed to be authoritative. For example, a system might use the Organizational Unit (OU) from a certificate to determine access policies and then embed that value in a JSON Web Token (JWT) claim without escaping. If the JWT processing logic interprets the claim as executable logic, an attacker could craft a certificate with a malicious payload such as "role": "admin; DROP TABLE users", leading to privilege escalation or data loss when the claim is evaluated.
Another vector involves template engines. If Axum passes mTLS fields into a rendering context (e.g., Askama or Tera), unescaped input can result in template injection. An attacker-controlled Common Name like {{7*'7'}} could execute arbitrary logic in the template, depending on the engine’s behavior. Because mutual TLS is often seen as a strong authentication mechanism, developers may overlook input validation, assuming the channel itself provides integrity. However, the channel authenticates the client; it does not guarantee the safety of the data carried in certificates.
Mutual Tls-Specific Remediation in Axum
Remediation focuses on treating mTLS-derived data as untrusted input, applying strict validation, and avoiding direct interpolation into commands, queries, or templates. Below are concrete Axum code examples demonstrating secure handling of mutual TLS certificates.
1. Sanitize certificate fields before use
Extract and validate certificate fields against a strict allowlist before any usage. For instance, if a Common Name is expected to match a known user identifier, enforce a regex pattern that excludes shell metacharacters or control characters:
use axum::{routing::get, Router, extract::ConnectInfo};
use axum::http::header::HeaderName;
use openssl::ssl::SslStream;
struct SslInfo {
peer_certificate: Option<Vec<u8>>
}
async fn safe_handler(ConnectInfo(info): ConnectInfo<SslInfo>) -> String {
let cn = extract_and_validate_cn(&info.peer_certificate)
.unwrap_or_else(|| "unknown".to_string());
// Safe: cn is guaranteed to match expected pattern
format!("User-{{}}", cn)
}
fn extract_and_validate_cn(cert_bytes: &Option<Vec<u8>>) -> Option<String> {
let cert = cert_bytes.as_ref()?;
// Decode PEM/DER and extract CN (simplified)
let cn = parse_certificate(cert).ok()?;
// Allow only alphanumeric, underscore, hyphen, dot
let re = regex::Regex::new(r"^[a-zA-Z0-9_.-]+$").ok()?;
if re.is_match(&cn) { Some(cn) } else { None }
}
2. Use parameterized APIs instead of shell commands
Avoid invoking shell commands with certificate-derived arguments. Prefer native APIs or parameterized queries. For example, instead of calling a shell script to fetch permissions, use a Rust function that maps the CN to permissions directly:
async fn get_permissions_safe(user_id: &str) -> Result<String, Box<dyn std::error::Error>> {
// Map user_id to permissions via a safe lookup, not shell
let permissions = match user_id {
"alice" => "read,write",
"bob" => "read",
_ => "none",
};
Ok(permissions.to_string())
}
async fn handler(ConnectInfo(info): ConnectInfo<SslInfo>) -> String {
let cn = extract_and_validate_cn(&info.peer_certificate).unwrap_or_default();
get_permissions_safe(&cn).await.unwrap_or_default()
}
3. Escape or reject input in templates
If using a template engine, ensure that mTLS fields are either escaped or that the engine’s auto-escaping is enabled. For Tera, configure the environment to autoescape and never pass raw certificate strings into template contexts without validation:
use tera::{Tera, Context};
let mut tera = Tera::default();
tera.add_template("user.html", "User: {{ cn | e }}").unwrap();
let mut ctx = Context::new();
ctx.insert("cn", &validated_cn); // validated_cn is safe
let rendered = tera.render("user.html", &ctx).unwrap();
These practices ensure that mutual TLS authentication enhances security without introducing injection risks through certificate metadata.