Double Free in Axum with Basic Auth
Double Free in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
A Double Free occurs when a program attempts to free the same dynamically allocated memory twice. In Rust web services built with the Axum framework, this typically arises from unsafe interactions between authentication state management and request handling logic. When Basic Auth is used, the runtime parses the Authorization header, decodes the credentials, and often materializes them into owned structures (e.g., a Credentials struct). If these structures are cloned, passed across async boundaries, or stored in shared state without careful ownership discipline, the same underlying buffer can be freed multiple times during drop, leading to memory unsafety.
Consider a scenario where an Axum handler clones a Credentials object into multiple futures, and those futures are stored in a shared Arc<Mutex<...>>. If the implementation inadvertently uses raw pointers or interacts with FFI that assumes exclusive ownership, the reference count may not correctly represent unique ownership. When the last reference drops, the memory is freed; however, if another reference erroneously believes it still owns the data and drops again, a double free occurs. This is especially relevant when Basic Auth data is combined with middleware that caches or reuses request parts across retries or redirects.
The risk is compounded because Basic Auth credentials are often decoded from base64 and stored in temporary buffers. If these buffers are moved into multiple asynchronous tasks without proper synchronization or if the developer uses Arc::clone on a structure containing a raw pointer to decoded bytes, the drop order becomes non-deterministic. The 12 security checks in middleBrick, including the Unsafe Consumption and BFLA/Privilege Escalation tests, are designed to detect patterns where authentication state is handled in ways that could lead to memory safety issues like Double Free, even in a memory-safe language like Rust.
In the context of an unauthenticated scan, middleBrick can identify risky patterns such as missing validation on the Authorization header, improper use of shared state, or lack of bounds checking on credential lengths. These findings map to the Input Validation and Property Authorization categories, highlighting where developer assumptions about ownership and lifetime may conflict with the runtime behavior of Basic Auth processing in Axum.
Basic Auth-Specific Remediation in Axum — concrete code fixes
To prevent Double Free and related memory safety issues when using Basic Auth in Axum, focus on clear ownership semantics and avoid unnecessary cloning of credential-containing structures. Prefer references over owned data where possible, and ensure that any shared state uses types that enforce single ownership or provide safe interior mutability without raw pointers.
Below are concrete, working examples of secure Basic Auth handling in Axum.
1. Minimal, safe extraction without shared state
Extract credentials per-request and avoid storing them globally.
use axum::{
async_trait,
extract::{FromRequest, Request},
http::header,
};
use base64::Engine;
struct Credentials {
username: String,
password: String,
}
enum AuthError {
MissingHeader,
InvalidFormat,
DecodeError,
}
#[async_trait]
impl FromRequest<S> for Credentials
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let header_value = req.headers().get(header::AUTHORIZATION)
.ok_or(AuthError::MissingHeader)?;
let header_str = header_value.to_str().map_err(|_| AuthError::InvalidFormat)?;
if !header_str.starts_with("Basic ") {
return Err(AuthError::InvalidFormat);
}
let encoded = header_str.trim_start_matches("Basic ");
let decoded = base64::engine::general_purpose::STANDARD.decode(encoded)
.map_err(|_| AuthError::DecodeError)?;
let parts: Vec<String> = String::from_utf8(decoded)
.map_err(|_| AuthError::DecodeError)?
.splitn(2, ':')
.map(|s| s.to_string())
.collect();
if parts.len() != 2 {
return Err(AuthError::InvalidFormat);
}
Ok(Credentials {
username: parts[0].clone(),
password: parts[1].clone(),
})
}
}
async fn handler(creds: Credentials) -> String {
format!("Authenticated as: {}", creds.username)
}
2. Using shared configuration safely with Arc
If you must share non-credential configuration (not the credentials themselves), use Arc on simple, cloneable data and keep credentials local to the handler.
use axum::{routing::get, Router};
use std::sync::Arc;
struct AppConfig {
app_name: String,
max_retries: u32,
}
async fn protected_handler(
creds: Credentials, // from previous extraction
config: Arc<AppConfig>,
) -> String {
format!("{} - {} (max retries: {})", config.app_name, creds.username, config.max_retries)
}
#[tokio::main]
async fn main() {
let config = Arc::new(AppConfig {
app_name: "MyService".to_string(),
max_retries: 3,
});
let app = Router::new()
.route("/secure", get(protected_handler))
.with_state(config);
// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
}
By following these patterns, you eliminate scenarios where the same decoded credential buffer could be freed multiple times. The middleBrick CLI can be used to verify these fixes: middlebrick scan <url>, and the GitHub Action can enforce that no risky authentication patterns reach production.