Timing Attack in Axum with Basic Auth
Timing Attack in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
A timing attack in Axum using HTTP Basic Auth arises because the server-side comparison of the client-supplied credentials against the expected values is typically performed in a way whose execution time depends on the supplied secret. In Basic Auth, the credentials are sent in the Authorization header as base64(username:password). Axum applications commonly extract the header, decode the payload, and then compare the username and password strings. If the comparison logic does not run in constant time, an attacker can measure response differences and gradually infer the correct value.
Consider a naive implementation where the developer compares the password using a simple equality check:
let expected_password = "correct_password";
if supplied_password == expected_password {
// grant access
}
Such a comparison short-circuits on the first mismatching byte, making the runtime distinguishable based on how many characters of the attacker’s guess match the expected password. An attacker can send many requests with varied passwords and observe timing differences to perform an offline guessing attack. Even if the password is protected by transport-layer encryption, the server’s processing still leaks information through timing variance.
Usernames can also be vulnerable if the lookup strategy depends on ordered data structures or conditional branching that varies by input. For example, searching through a list of users sequentially and returning immediately when a match is found creates a timing side-channel that depends on the position of the correct user. In an environment where an attacker can make numerous authenticated-looking requests (using malformed but syntactically valid credentials), these subtle timing differences can be amplified across the network, especially when combined with techniques to reduce noise.
The vulnerability is compounded when the Basic Auth credentials are reused across multiple endpoints or when the same secret is used for both authentication and authorization checks without cryptographic safeguards. An attacker who infers the password might also exploit weak session management or missing protections around token handling. Because Axum does not enforce any particular comparison strategy by default, developers must explicitly adopt constant-time comparison primitives and secure credential storage to mitigate timing-based inference.
Basic Auth-Specific Remediation in Axum — concrete code fixes
To defend against timing attacks in Axum, ensure that credential comparisons execute in constant time and that secrets are stored and handled securely. The following example demonstrates a safer approach using constant-time comparison and secure password storage.
use axum::{
async_trait,
extract::{FromRequest, Request},
http::{
header::{AUTHORIZATION, HeaderMap},
HeaderValue,
},
response::IntoResponse,
Extension,
};
use base64::prelude::*;
use constant_time_eq::constant_time_eq;
use std::net::SocketAddr;
use tower_http::services::ServeDir;
struct BasicAuthCredentials {
username: String,
password_hash: Vec<u8>, // store as a hash, not plaintext
}
#[async_trait]
impl FromRequest<S> for BasicAuthCredentials
where
S: Send + Sync,
{
type Rejection = (axum::http::StatusCode, &'static str);
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let headers = req.headers();
let auth_header = headers.get(AUTHORIZATION)
.ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing Authorization header"))?;
let auth_str = auth_header.to_str().map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid header encoding"))?;
if !auth_str.starts_with("Basic ") {
return Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid auth scheme"));
}
let encoded = &auth_str[6..];
let decoded = BASE64_STANDARD.decode(encoded).map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid base64"))?;
let parts = std::str::from_utf8(&decoded).map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid UTF-8"))?;
let mut split = parts.splitn(2, ':');
let username = split.next().ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing username"))?;
let password = split.next().ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing password"))?;
// In practice, fetch the user record and compare hashes
let stored_hash = fetch_user_hash(username).await;
if constant_time_eq(password.as_bytes(), &stored_hash) {
Ok(BasicAuthCredentials { username: username.to_string(), password_hash: stored_hash })
} else {
Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid credentials"))
}
}
}
async fn fetch_user_hash(username: &str) -> Vec<u8> {
// Replace with secure lookup and hashing, e.g., bcrypt or argon2
// This is a placeholder returning a fixed hash for demonstration
if username == "alice" {
b"hashed_password_placeholder".to_vec()
} else {
vec![]
}
}
#[tokio::main]
async fn main() {
let app = axum::Router::new()
.route("/protected", axum::routing::get(|| async { "OK" }))
.layer(Extension(()));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Key practices include:
- Use a constant-time equality function (e.g.,
constant_time_eq) to compare secrets, preventing early-exit timing differences. - Store passwords using a strong, slow key-derivation function (e.g., bcrypt, argon2) and compare the derived hashes rather than plaintext passwords.
- Ensure username lookup does not branch on presence; use a fixed-time map access pattern or treat missing users identically to avoid timing leaks about valid usernames.
- Limit the information in error responses to avoid aiding attackers in distinguishing between missing users and incorrect passwords.
These measures reduce the feasibility of timing-based inference by removing observable timing variance in the authentication path.