Format String in Axum with Api Keys
Format String in Axum with Api Keys — how this combination creates or exposes the vulnerability
A format string vulnerability occurs when user-controlled input is passed directly into a formatting function such as format! or write! without explicit format specification. In Axum, this risk can manifest when API keys or other request parameters are interpolated into log messages, error responses, or dynamic strings. For example, if an API key provided by a client is used to construct a log entry using a format string that relies on user input to determine formatting, an attacker can supply format verbs such as %s, %x, or %n to read stack memory or cause writes.
Consider an endpoint that logs received API keys for debugging:
let api_key = query_params.get("api_key").unwrap_or("");
info!("Received API key: {}", api_key); // Safe when using structured logging
This pattern is safe if the logging macro uses explicit formatting. However, a vulnerable pattern looks like this:
let user_input = query_params.get("format").unwrap_or("{}");
let message = format!(user_input, "test"); // UNSAFE: user_input controls the format string
If user_input contains format specifiers and an API key is also reflected in the response or logs, an attacker may read sensitive data or cause crashes. In the context of API keys, an attacker might submit a crafted format string in a header or query parameter to probe memory contents, potentially extracting parts of the API key or other secrets from the stack. Because Axum applications often integrate structured logging and validation layers, the risk is amplified when developers inadvertently pass unchecked user input into formatting routines that also handle authentication values.
Additionally, if an error response includes user-controlled data using unchecked format strings, it can lead to information disclosure. For instance, returning a message built with format!(user_provided_fmt, key=api_key) can expose the API key through format injection. The combination of dynamic formatting and sensitive authentication material like API keys creates a scenario where confidentiality and stability of the API can be compromised.
Api Keys-Specific Remediation in Axum — concrete code fixes
To remediate format string risks when handling API keys in Axum, always use explicit format strings and avoid passing user input directly to formatting macros. Prefer structured logging and strict input validation.
Safe logging of API keys
Use a logging framework that supports structured fields rather than string interpolation:
use tracing::info;
async fn handler(
Query(params): Query<HashMap impl IntoResponse {
if let Some(api_key) = params.get("api_key") {
info!(api_key = &api_key[..], "API key received"); // Structured field, safe
// Validate and use the key
}
}
Avoiding format string injection
Never use user input as the format string. Instead, use fixed format strings and pass user data as arguments:
let user_fmt = query_params.get("fmt").unwrap_or("{}");
// Instead of: format!(user_fmt, api_key)
let safe_message = format!("{}", api_key); // Fixed format, user data as argument
Validation and rejection of suspicious input
Validate API keys and reject inputs that contain format specifiers when they are not expected:
fn is_valid_api_key(key: &str) -> bool {
!key.contains('%') && key.len() == 32
}
async fn handler(
Query(params): Query<HashMap<String, String>>
) -> Result<impl IntoResponse, (StatusCode, String)> {
let api_key = params.get("api_key").ok_or_else(|| (StatusCode::BAD_REQUEST, "Missing key".into()))?;
if !is_valid_api_key(api_key) {
return Err((StatusCode::BAD_REQUEST, "Invalid key".into()));
}
// Proceed safely
Ok((StatusCode::OK, "Valid key"))
}
Using proper escaping for dynamic content
If you must construct dynamic strings, ensure that format specifiers are escaped or use a templating engine with strict mode. For responses, prefer JSON serialization which does not involve format strings:
use axum::Json;
use serde_json::json;
async fn respond_with_key(
Query(params): Query<HashMap<String, String>>
) -> Json<serde_json::Value> {
let api_key = params.get("api_key").unwrap_or("unknown");
Json(json!({ "api_key": api_key })) // No format string involved
}