Server Side Template Injection in Axum with Dynamodb
Server Side Template Injection in Axum with Dynamodb — how this specific combination creates or exposes the vulnerability
Server Side Template Injection (SSTI) occurs when an attacker can inject template expressions that are evaluated on the server. In Axum, this typically arises when user-controlled input is passed into a templating engine such as Askama or Tera and then used to render responses. If the rendered output is subsequently stored in or retrieved from DynamoDB, the vulnerability can lead to unexpected behavior, data leakage, or exposure of internal context depending on how the template is processed and cached.
Consider an Axum handler that accepts a user-supplied user_id, queries DynamoDB for a user record, and then uses a template to format a response. If the template is constructed dynamically using the user input—for example, embedding user_id into a Jinja-like string that is later rendered—malicious payloads such as {{7*7}} or {% import os %}{{os.environ}} can be evaluated on the server side. Even though DynamoDB itself is a NoSQL database and does not execute templates, the combination of Axum routing, a vulnerable templating engine, and DynamoDB as a data source creates a chain where untrusted data influences server-side evaluation.
An example scenario: an endpoint accepts a name parameter, queries DynamoDB for a profile, and renders a greeting via a template. If the name is not sanitized and is directly interpolated into the template string before rendering, an attacker could supply {{7*"10"}} or template-specific syntax to cause unintended evaluation. DynamoDB may return the attacker-controlled data, but the risk emerges when that data is re-used in a server-side template context without proper escaping or sandboxing.
Because middleBrick tests unauthenticated attack surfaces and includes input validation checks, it can detect SSTI patterns in API responses that involve template rendering. The scanner’s LLM/AI Security checks are especially relevant here, as they probe for prompt injection and output anomalies that can indicate template misuse when APIs interact with generative models or external services.
Remediation guidance centers on strict input validation, avoiding dynamic template construction from user data, and using sandboxed or precompiled templates. Leveraging Axum extractors to enforce strong typing and schema validation before any data reaches a templating engine reduces the attack surface. Complementary use of middleBrick’s dashboard and CLI can help track these issues over time and integrate checks into CI/CD pipelines, ensuring that risky patterns are flagged before deployment.
Dynamodb-Specific Remediation in Axum — concrete code fixes
To prevent SSTI in Axum when working with DynamoDB, ensure that user input never reaches the templating engine as executable logic. Instead, treat DynamoDB records as plain data and apply strict serialization rules. Below are concrete, safe patterns for Axum handlers that query DynamoDB and render responses.
1. Use static templates with context-only data
Define templates with fixed structure and only inject data values, never logic. With Askama, this means using Jinja-like syntax for variables only and disabling autoescape bypasses.
// templates/greeting.html
<p>Hello, {{ name }}</p>
// Rust handler using askama
use askama::Template;
use axum::{routing::get, Router};
#[derive(Template, Debug)]
#[template(path = "greeting.html")]
struct GreetingTemplate {
name: String,
}
async fn handler(
Query(params): Query<HashMap<String, String>>
) -> String {
let name = params.get("name").cloned().unwrap_or_else(|| "World".to_string());
// Validate/sanitize name here if needed
let template = GreetingTemplate { name };
template.render().unwrap_or_else(|_| "Error rendering template".to_string())
}
let app = Router::new().route("/greet", get(handler));
2. Validate and sanitize DynamoDB responses before rendering
When retrieving items from DynamoDB, use strongly-typed structures and avoid passing raw attribute maps into templates. This prevents attacker-controlled keys or values from being interpreted as template code.
// models.rs
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct UserProfile {
user_id: String,
display_name: String,
}
// handler.rs
use aws_sdk_dynamodb::Client;
use axum::{routing::get, Json};
async fn get_user_profile(
client: &Client,
user_id: String,
) -> Result<UserProfile, aws_sdk_dynamodb::Error> {
let resp = client
.get_item()
.table_name("Profiles")
.key("user_id", aws_sdk_dynamodb::types::AttributeValue::S(user_id))
.send()
.await?
.item
.ok_or_else(|| aws_sdk_dynamodb::error::SdkError::ConstructionFailure("item not found".into()))?;
let profile = UserProfile {
user_id: resp.get("user_id").and_then(|v| v.as_s().ok()).unwrap_or(&"".to_string()).to_string(),
display_name: resp.get("display_name").and_then(|v| v.as_s().ok()).unwrap_or(&"".to_string()).to_string(),
};
Ok(profile)
}
async fn profile_handler(
Path(user_id): Path<String>,
client: Extension<Client>
) -> String {
match get_user_profile(&client, user_id).await {
Ok(profile) => format!("User: {} ({})", profile.display_name, profile.user_id),
Err(_) => "Profile not found".to_string(),
}
}
3. Avoid eval-like behavior in custom formatting
If you must support limited variable substitution, use a dedicated escaping layer and whitelist allowed patterns. Never eval or interpret user input as Rust code or template directives.
// safe_format.rs
fn safe_format(pattern: &str, name: &str) -> String {
// Only allow alphanumeric names; reject anything that looks like template syntax
if name.chars().all(|c| c.is_alphanumeric() || c == ' ') {
pattern.replace("{{name}}", name)
} else {
"Invalid input".to_string()
}
}
// Usage in handler
let body = safe_format("Hello, {{name}}", &user_supplied_name);
4. Enforce schema validation on query parameters
Use Axum extractors to validate and constrain input before it touches business logic or templates. This reduces the risk of injection through malformed or unexpected data.
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct NameQuery {
name: String,
}
async fn safe_greet(Query(Query { name }): Query<NameQuery>) -> String {
// name is already validated as a string by serde; apply further checks if needed
format!("Hello, {}", name)
}