Timing Attack in Axum with Api Keys
Timing Attack in Axum with Api Keys — how this specific combination creates or exposes the vulnerability
A timing attack in an Axum service that uses API keys can occur when key validation logic does not execute in constant time. In Axum, route handlers typically extract an API key from a header (e.g., x-api-key) and compare it against a stored value. If the comparison short-circuits on the first mismatching character, an attacker can measure response times to infer information about the correct key. This is a classic authentication side-channel: the server may respond faster when the initial bytes match, allowing an attacker to iteratively guess the key byte by byte.
Why this combination is notable:
- Axum is a Rust web framework where handlers are composed; if key validation is performed manually or via naive string equality, the comparison is vulnerable unless explicitly designed to be constant-time.
- API keys are often high-entropy strings, but their handling in authentication middleware or custom guards must avoid branching on secret data. Even with a correct cryptographic primitive, improper use (e.g., early return on mismatch) reintroduces timing leakage.
- The attack surface is unauthenticated: middleBrick scans the endpoint without credentials and can detect timing differences by observing response variations across crafted requests, aligning with its authentication and input validation checks.
Example of vulnerable Axum code:
use axum::{routing::get, Router};
use std::net::SocketAddr;
async fn handler(
headers: axum::http::HeaderMap,
) -> &'static str {
let api_key = headers.get("x-api-key").and_then(|v| v.to_str().ok());
match api_key {
Some(k) if k == "supersecretkey123" => "OK",
_ => "Unauthorized",
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/protected", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
In this example, the k == "supersecretkey123" comparison is not constant-time. An attacker can send many requests with keys that vary byte-by-byte and measure response times to deduce the correct key. middleBrick’s authentication checks aim to surface such patterns by testing unauthenticated endpoints for inconsistent timing behavior.
How middleBrick relates:
- middleBrick runs 12 security checks in parallel, including Authentication and Input Validation, which can flag timing-related anomalies during scans.
- Its LLM/AI Security module does not apply here, as this scenario is not about prompt injection or model behavior but about cryptographic hygiene in request validation.
Api Keys-Specific Remediation in Axum — concrete code fixes
To mitigate timing attacks with API keys in Axum, ensure comparisons are constant-time and avoid branching on secret data. Use a fixed-time equality check and return uniform responses regardless of where the mismatch occurs. Below are concrete, working examples.
Remediation 1: Constant-time comparison using subtle crate
Add subtle to your dependencies (Cargo.toml):
[dependencies]
axum = "0.6"
subtle = "2.4"
tokio = { version = "1", features = ["full"] }
Then implement the handler:
use axum::{routing::get, Router};
use subtle::ConstantTimeEq;
use std::net::SocketAddr;
async fn handler(
headers: axum::http::HeaderMap,
) -> &'static str {
let api_key = headers.get("x-api-key").and_then(|v| v.to_str().ok());
let expected = b"supersecretkey123";
let input = api_key.map(|s| s.as_bytes()).unwrap_or(&[]);
// Ensure lengths are also checked in constant time by comparing up to max length
let mut ok = subtle::ConstantTimeEq::ct_eq(&input.len().to_be_bytes(), &expected.len().to_be_bytes());
for (a, b) in input.iter().zip(expected.iter()) {
ok &= a.ct_eq(b);
}
if ok.into() {
"OK"
} else {
// Return a generic response with similar timing characteristics
"Unauthorized"
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/protected", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Remediation 2: Use a cryptographic hashed comparison with fixed-time verification
Store a hash of the API key (e.g., SHA-256) and compare hashes in constant time. This way the raw key is never directly compared, and the hash comparison is constant-time.
use axum::{routing::get, Router};
use digest::Digest;
use subtle::ConstantTimeEq;
use std::net::SocketAddr;
async fn handler(
headers: axum::http::HeaderMap,
) -> &'static str {
let api_key = headers.get("x-api-key").and_then(|v| v.to_str().ok());
let mut hasher = sha2::Sha256::new();
hasher.update(b"supersecretkey123");
let expected_hash = hasher.finalize();
let input = api_key.map(|s| s.as_bytes()).unwrap_or(&[]);
let mut input_hasher = sha2::Sha256::new();
input_hasher.update(input);
let input_hash = input_hasher.finalize();
let mut ok = subtle::ConstantTimeEq::ct_eq(&input_hash[..], &expected_hash[..]);
if ok.into() {
"OK"
} else {
"Unauthorized"
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/protected", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Additional guidance:
- Ensure response paths and error messages are consistent to prevent timing or behavioral leaks.
- Validate input length and structure before comparison to avoid secondary timing channels.
- middleBrick’s Pro plan includes continuous monitoring and GitHub Action integration to detect regressions in authentication logic during CI/CD, helping to catch accidental reintroduction of timing vulnerabilities.