Format String in Axum
How Format String Manifests in Axum
Format string vulnerabilities in Axum applications typically arise when user-controlled input is passed directly to formatting macros like format!, println!, or write! without proper sanitization. While Rust's compile-time checks prevent many classic format string issues, runtime format string vulnerabilities can still occur when format strings are constructed dynamically from user input.
In Axum, this often manifests in error handling, logging, or response generation. Consider an endpoint that logs request data:
use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use std::fmt;
async fn log_user(Path(user_id): Path<i32>) -> impl IntoResponse {
let format_string = format!("User {} accessed the system", user_id);
println!("{}", format_string); // Safe: format! returns a String
// Vulnerable pattern
let user_input = get_user_input(); // Assume this comes from request
println!("{}", user_input); // Safe: {} is literal
// Actual vulnerability
println!(user_input); // DANGER: user_input treated as format string
(StatusCode::OK, format!("Logged user {}", user_id))
}
The critical vulnerability occurs when println!(user_input) is called because Rust's formatting machinery interprets the contents of user_input as format specifiers. An attacker could send %n or other format directives to manipulate memory or cause panics.
In Axum route handlers, this often appears when:
- Logging request parameters directly without escaping
- Constructing error messages with user input as the format string
- Generating responses where user input controls format specifiers
- Using
format!with dynamic format strings
The vulnerability is particularly dangerous in Axum because handlers often process JSON, query parameters, or path segments that can contain format string characters. For example:
async fn vulnerable_handler(
Query(params): Query<HashMap<String, String>>
) -> impl IntoResponse {
let format_str = params.get("message").unwrap_or("Default message");
println!(format_str); // Vulnerable if format_str contains % directives
(StatusCode::OK, format!("Message logged"))
}
This pattern is common in debugging endpoints or admin interfaces where developers assume user input is safe for logging.
Axum-Specific Detection
Detecting format string vulnerabilities in Axum requires both static analysis and runtime scanning. Static analysis tools can identify dangerous patterns where format strings are constructed from user input. Look for:
- Format macros (
format!,println!,write!) with variables that could contain user input - Dynamic construction of format strings using
format!()or string concatenation - Unescaped user input passed to logging macros
middleBrick's API security scanner specifically detects format string vulnerabilities in Axum applications by analyzing the runtime behavior of endpoints. The scanner tests for:
// What middleBrick tests for:
// 1. Basic format string injection
POST /api/test HTTP/1.1
Content-Type: application/json
{"format": "%x%x%x"}
// 2. Memory disclosure attempts
{"format": "%p%p%p"}
// 3. Write attempts
{"format": "test%n"}
The scanner identifies vulnerable endpoints by sending specially crafted payloads and observing the application's response. middleBrick's LLM security module also checks for format string vulnerabilities in AI-related endpoints, as these often process user prompts that could contain format directives.
For Axum applications, middleBrick provides specific findings:
- Identifies which routes accept user-controlled format strings
- Tests for memory disclosure via
%p,%x,%sspecifiers - Checks for write operations via
%n - Provides remediation guidance specific to Rust/Axum patterns
The scanner's 12 security checks include Input Validation testing that specifically targets format string injection, and the Inventory Management check identifies all API endpoints that could be vulnerable to this class of attack.
Axum-Specific Remediation
Remediating format string vulnerabilities in Axum requires a defense-in-depth approach. The primary strategy is to never use user input as a format string. Instead, always use static format strings and pass user data as arguments:
// Vulnerable
println!(user_input);
// Secure
println!("{}", user_input);
// Even better with explicit formatting
println!("{:?}", user_input); // Debug formatting
println!("{}", sanitize(user_input)); // Sanitization
For Axum handlers, implement input validation and sanitization:
use axum::extract::Path;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use std::fmt;
// Sanitization function
fn sanitize_format_string(input: &str) -> String {
// Remove or escape format specifiers
input.replace('%', "%%").replace('{', "{{").replace('}', "}}")
}
async fn secure_handler(
Path(user_input): Path<String>
) -> impl IntoResponse {
let safe_input = sanitize_format_string(&user_input);
// Always use static format strings
println!("User input: {}" , safe_input);
(StatusCode::OK, format!("Input processed: {}" , safe_input))
}
For logging in Axum applications, use structured logging libraries that don't rely on format strings:
use tracing::{info, warn};
async fn logging_handler(
Query(params): Query<HashMap<String, String>>
) -> impl IntoResponse {
// Structured logging - no format strings
info!("Request received");
info!(?params, "Query parameters");
// Or use a logging crate that escapes format strings
let message = params.get("message").unwrap_or("");
println!("{}", sanitize_format_string(message));
(StatusCode::OK, "Logged")
}
middleBrick's remediation guidance includes specific recommendations for Axum applications, such as:
- Always validate and sanitize user input before logging
- Use
tracingcrate for structured logging - Implement input validation middleware that checks for format specifiers
- Configure logging to escape special characters
For production Axum applications, consider adding a middleware that automatically sanitizes format strings:
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum::http::Request;
async fn format_string_middleware(
req: Request,
next: Next,
) -> impl IntoResponse {
// Check for format string patterns in request
let body = hyper::body::to_bytes(req.body()).await.unwrap();
let body_str = String::from_utf8_lossy(&body);
if contains_format_string(&body_str) {
return (StatusCode::BAD_REQUEST, "Invalid format string");
}
next.run(req).await
}
fn contains_format_string(input: &str) -> bool {
// Simple pattern matching for format specifiers
input.contains('%') || input.contains('{')
}