Server Side Template Injection in Axum with Mutual Tls
Server Side Template Injection in Axum with Mutual Tls
Server Side Template Injection (SSTI) occurs when an attacker can control template input that is later rendered by a server-side templating engine. In Axum, this typically arises when user-supplied data is passed into a template context and then rendered with a crate like askama or tera without proper escaping or sandboxing. When Mutual Transport Layer Security (Mutual Tls) is used, the server validates client certificates, which may give developers a false sense of security. Enabling Mutual Tls ensures strong client authentication but does not reduce risks from untrusted data that reaches application logic or templates. Therefore, even with Mutual Tls enforced at the transport layer, routes that accept, transform, and forward data to templates remain vulnerable if input validation and output encoding are omitted.
Consider an Axum handler that accepts a JSON payload containing a user-controlled string and passes it directly into a template. For example:
use axum::{routing::get, Router, extract::State, http::StatusCode};
use askama::Template;
#[derive(Template)]
#[template(path = "greeting.html")]
struct GreetingTemplate {
name: String,
}
async fn handler(
State(tmpl): State<askama::Loader>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>
) -> Result<impl axum::response::IntoResponse, (StatusCode, String)> {
let name = params.get("name").cloned().unwrap_or_default();
let template = GreetingTemplate { name };
let rendered = template.render().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(rendered)
}
If the name parameter contains a template expression such as {{ 7 * 8 }} or a more dangerous payload that accesses internal methods, the templating engine may evaluate it, leading to unintended behavior or data exposure. With Mutual Tls, the client identity is verified, but this does not sanitize the content of the request body. Attackers who can intercept or authenticate with a valid client certificate can still probe the application with malicious inputs if the endpoint is reachable. This combination can amplify exposure because developers might assume that Mutual Tls alone protects backend logic, while the actual injection occurs at the template layer.
Another scenario involves OpenAPI/Swagger-driven code generation where templates are rendered with user-provided metadata. If generated code does not enforce strict schema validation and escaping, attackers can exploit SSTI to achieve cross-site scripting or, in server-side contexts, trigger local file inclusion depending on the template engine. Since middleBrick scans test the unauthenticated attack surface and include OpenAPI spec analysis, such misconfigurations can be detected by correlating spec definitions with runtime behavior.
Mutual Tls-Specific Remediation in Axum
Remediation focuses on strict input validation, output encoding, and minimizing template exposure. Even with Mutual Tls enforcing client authentication, user-controlled data must be treated as untrusted. Use allowlists for expected values, avoid passing raw user input directly into template contexts, and leverage type-safe templates with automatic escaping.
Below are concrete Axum code examples demonstrating secure handling with Mutual Tls. First, enforce Mutual Tls at the TLS layer using rustls. This configuration ensures client certificates are validated before requests reach Axum handlers:
use axum::Server;
use std::net::SocketAddr;
use tokio_rustls::rustls::{ServerConfig, Certificate, PrivateKey};
use tokio_rustls::TlsAcceptor;
use std::sync::Arc;
async fn run_axum_with_mtls() {
let certs = load_certs("ca.crt").expect("failed to load CA cert");
let mut keys = load_keys("server.key").expect("failed to load server key");
let client_auth = tokio_rustls::rustls::ClientAuthMode::VerifyAndRequireClientAuth;
let config = ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(client_auth)
.with_single_cert(certs, keys.remove(0))
.expect("invalid key or cert");
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let app = Router::new().route("/greet", get(handler));
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
Server::bind(&addr)
.tls_config(tls_acceptor)
.unwrap()
.serve(app.into_make_service())
.await
.unwrap();
}
Second, in handlers, avoid directly injecting raw query or body parameters into templates. Instead, validate and transform them:
use axum::{routing::post, extract::State, http::StatusCode, Json};
use serde::Deserialize;
use askama::Template;
#[derive(Deserialize)]
struct UserInput {
name: String,
}
#[derive(Template)]
#[template(path = "safe_greeting.html", escape = "html")]
struct SafeGreeting {
name: String,
}
async fn safe_handler(
State(tmpl): State<askama::Loader>,
Json(payload): Json<UserInput>
) -> Result<impl axum::response::IntoResponse, (StatusCode, String)> {
if !payload.name.chars().all(|c| c.is_alphanumeric() || c.is_whitespace()) {
return Err((StatusCode::BAD_REQUEST, "Invalid name"[..].into()));
}
let template = SafeGreeting { name: payload.name };
let rendered = template.render().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(rendered)
}
By combining Mutual Tls for transport assurance with strict input validation and escaping-aware templates, you reduce the attack surface for SSTI. middleBrick can help verify that endpoints using Mutual Tls are not inadvertently exposing template injection risks through unvalidated data paths.