Axum API Security
Axum Security Posture
Axum, the Rust web framework built on Tokio, offers strong security foundations through Rust's memory safety guarantees and type system. However, like any framework, Axum's security posture depends heavily on how developers configure and use it.
By default, Axum provides several security advantages: no global state mutations without explicit handling, compile-time guarantees against many common bugs, and no reflection-based deserialization vulnerabilities. The framework's async-first design also helps prevent thread-related security issues common in traditional web servers.
The framework's minimalism is both a strength and weakness. While it avoids shipping with potentially vulnerable middleware, developers must manually implement critical security controls. Axum doesn't include built-in protections against common API vulnerabilities like rate limiting, authentication middleware, or request validation—these must be added deliberately.
Key security considerations for Axum applications include proper error handling (Axum's default error responses can leak implementation details), input validation (the framework doesn't sanitize inputs), and secure defaults for HTTP headers and CORS policies.
Top 5 Security Pitfalls in Axum
Understanding where Axum applications commonly fail helps developers avoid these mistakes. Here are the five most frequent security issues we observe:
- Missing Authentication Middleware - Axum's extractors make it easy to forget authentication. Developers often extract sensitive data without verifying user identity first, leading to unauthorized access.
- Insecure Default Error Handling - Axum's default error responses include stack traces and internal details. Without proper error mapping, attackers can enumerate endpoints and discover implementation details.
- Unsafe JSON Deserialization - While Rust's type system helps, developers still use
serde_json::from_slicewithout validation, allowing attackers to craft malicious payloads that trigger logic bugs or DoS conditions. - Missing Rate Limiting - Axum doesn't include rate limiting by default. Brute force attacks, API abuse, and resource exhaustion are common when this protection is absent.
- Incomplete CORS Configuration - Developers often use wildcard origins (
*) for convenience during development, which remains in production code, exposing APIs to cross-origin attacks.
Security Hardening Checklist
Implement these security controls to significantly improve your Axum API's security posture:
Authentication & Authorization
use axum::extract::{Extension, Path};
use jsonwebtoken::{decode, DecodingKey, Validation};
async fn auth_required(
Extension(token): Extension<String>,
db: Extension<PgPool>,
) -> Result<impl IntoResponse> {
let validation = Validation::new(jsonwebtoken::Algorithm::HS256);
let decoded = decode::(&token, &DecodingKey::from_secret(secret), &validation)?;
// Verify user exists and has permissions
let user = get_user_from_db(&db, decoded.claims.sub).await?;
Ok(Json(user))
}
Input Validation
use axum::extract::{Json, Path};
use serde::Deserialize;
use validator::{Validate, ValidationError};
#[derive(Deserialize, Validate)]
struct CreateUserRequest {
#[validate(length(min = 3, max = 50))]
username: String,
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
async fn create_user(
Json(req): Json<CreateUserRequest>,
) -> Result<impl IntoResponse> {
req.validate()?; // Validate before processing
// Proceed with business logic
Ok(Json({"status": "created"}))
}
Error Handling
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::error_handling::Error;
// Custom error type
#[derive(Debug)]
struct AppError(String);
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (res, _) = (StatusCode::INTERNAL_SERVER_ERROR, self.0)
.into_response();
res
}
}
// Global error handler
async fn handle_error(err: Error) -> impl IntoResponse {
match err {} // Handle specific error types if needed
}
Rate Limiting
use tower_http::services::ServeDir;
use tower_http::rate_limit::RateLimitLayer;
let rate_limit = RateLimitLayer::new(
std::num::NonZeroUsize::new(100).unwrap(), // 100 requests
std::time::Duration::from_secs(60), // per minute
);
let app = Router::new()
.route("/api/", get(api_handler))
.layer(rate_limit);
Security Headers
use tower_http::set_header::SetHeaderLayer;
use tower_http::headers::authorization::RequiredAuthorization;
let security_headers = SetHeaderLayer::if_not_present(
"X-Content-Type-Options",
"nosniff",
);
let cors = tower_http::cors::CorsLayer::new()
.allow_origin("https://yourdomain.com")
.allow_methods(["GET", "POST", "PUT", "DELETE"]);
let app = Router::new()
.route(...)
.layer(cors)
.layer(security_headers);