Server Side Template Injection in Axum with Api Keys
Server Side Template Injection in Axum with Api Keys — how this specific combination creates or exposes the vulnerability
Server Side Template Injection (SSTI) occurs when an attacker can inject template expressions that are later evaluated by a server-side templating engine. In Axum, this risk can emerge when Api Keys are used for authorization but the application passes untrusted input into a template rendering step, such as with Askama or similar engines. If user-controlled data (e.g., a query parameter, header, or JSON field) is included in the template context and not properly sanitized, an attacker may craft payloads that execute arbitrary template logic, leading to information disclosure or code execution.
Consider an endpoint that retrieves an Api Key from a request header and then renders it inside a template for debugging or logging purposes:
use axum::{{
routing::get,
Router,
extract::State,
http::HeaderMap,
}};
use askama::Template;
#[derive(Template)]
#[template(path = "key_status.html")]
struct KeyStatusTemplate {
api_key: String,
}
async fn status_handler(
headers: HeaderMap,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let api_key = headers.get("X-API-Key")
.map(|v| v.to_str().unwrap_or("").to_string())
.unwrap_or_else(|| "missing".to_string());
let template = KeyStatusTemplate { api_key };
let rendered = template.render().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Response::builder().body(rendered).unwrap())
}
If the template key_status.html directly interpolates {{ api_key }} without escaping, an attacker supplying {{7*7}} or a more advanced payload could trigger unintended behavior in the templating engine. In Askama, which compiles templates at build time, basic escaping is enforced for variables marked with {{ }}, but if the developer disables autoescape or uses raw blocks, the risk increases. Moreover, if the Api Key is used to dynamically select a template or influence control flow (e.g., conditional blocks based on key validity), injection could allow an attacker to access other templates or sensitive sections of the rendered output. This is especially dangerous when combined with verbose error messages that reveal filesystem paths or template internals, aiding further exploitation.
The presence of Api Keys does not cause SSTI by itself, but it can increase the impact: an exposed key might be used to impersonate services or access downstream systems. Because SSTI in Axum often stems from improper handling of user data in template contexts rather than the key validation logic, the vulnerability remains in the rendering layer. The combination of per-request Api Key extraction and insufficient input sanitization creates a scenario where an authenticated-looking request can abuse the template engine, making detection harder for scanners that do not test authorization boundaries in rendering paths.
Api Keys-Specific Remediation in Axum — concrete code fixes
To mitigate SSTI in Axum when using Api Keys, ensure that user input never reaches the template engine as executable logic. Treat headers, query parameters, and JSON bodies as untrusted, and apply strict validation and escaping. Below are concrete, safe patterns.
1. Strict header validation and no template injection
Do not pass raw header values into templates. Instead, validate the Api Key against a known set or pattern, and only pass safe metadata to the template.
use axum::{{
extract::Header,
routing::get,
Router,
response::Html,
}};
use askama::Template;
#[derive(Template)]
#[template(path = "key_status_safe.html")]
struct KeyStatusSafeTemplate {
is_valid: bool,
}
async fn status_handler_safe(
Header(headers): Header<_, _>,
) -> Result<Html<String>, (StatusCode, String)> {
let api_key = headers.get("X-API-Key")
.and_then(|v| v.to_str().ok())
.filter(|&k| k.len() == 32 && k.chars().all(|c| c.is_ascii_alphanumeric()))
.is_some();
let template = KeyStatusSafeTemplate { is_valid: api_key };
let rendered = template.render().map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Html(rendered))
}
In this approach, the raw key is never placed into the template context. The template only receives a boolean, eliminating any possibility of template injection via the key value.
2. Use autoescape and avoid raw blocks
When you must include user data, configure your template engine to autoescape by default and avoid raw blocks. For Askama, ensure your templates use the default autoescape behavior:
<!-- key_status.html -->
<p>Key status: {{ api_key_status }}</p>
Even if api_key_status contains untrusted strings, Askama will HTML-escape them, neutralizing injection vectors. Do not use {%- raw %}...{% endraw %} blocks with untrusted input.
3. Centralized validation middleware
Move key validation into middleware so templates only see verified, minimal data:
use axum::{{
extract::Extension,
middleware::Next,
body::Body,
http::Request,
response::Response,
}};
use std::sync::Arc;
async fn api_key_middleware(
Extension(state): Extension<Arc<AppState>>,
mut req: Request<Body>,
next: Next<Body>
) -> Response {
if let Some(key) = req.headers().get("X-API-Key") {
if is_valid_key(key, &state.valid_keys) {
return next.run(req).await;
}
}
Response::builder().status(StatusCode::UNAUTHORIZED).body(Body::empty()).unwrap()
}
With this pattern, templates receive only pre-validated flags or sanitized representations, and SSTI risks are minimized. The middleware approach also aligns with the principle that Api Key handling should be separate from presentation logic.