Api Rate Abuse in Axum with Basic Auth
Api Rate Abuse in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability
Rate abuse in an Axum service that uses HTTP Basic Auth can occur when authentication is applied only after request counting or when rate limits are enforced per IP rather than per identity. Basic Auth sends credentials on every request, but if the rate limiter does not incorporate the authenticated identity, an attacker can exhaust the shared limit for all users behind a single IP (e.g., NAT or shared gateway). This exposes the API to denial-of-service for legitimate users and can be a precursor to credential-guessing attacks if failed auth responses are not similarly rate-limited.
Consider an endpoint protected with Basic Auth where the rate limit is applied globally without factoring in the username. An attacker can send many unauthenticated or invalid-authentication requests that consume the shared quota, preventing authorized users from accessing the service. Even when authentication succeeds, inconsistent limits between authentication and authorization layers can allow authenticated users to exceed intended operation rates. For example, an endpoint with per-user limits should enforce those limits after successful auth, not rely solely on IP-based throttling that an attacker can bypass by rotating IPs or using a botnet.
Insecure implementation patterns in Axum can exacerbate the issue. If middleware ordering does not prioritize authentication before rate checks, or if the rate-limiting key omits the user identity (e.g., only using the client IP), the protection is ineffective. Moreover, Basic Auth does not inherently protect against replay or brute-force attempts; without additional mechanisms such as nonce, timestamp, or incrementing counters, repeated requests with the same credentials can be replayed to abuse rate-limited operations. The combination of per-IP rate limits and Basic Auth creates a weak boundary where authenticated identity is not part of the limiting key, enabling an attacker to saturate the allowance for legitimate clients.
These patterns map to common API security risks outlined in checks such as BFLA/Privilege Escalation and Rate Limiting. For instance, if an authenticated user can perform destructive actions at a high rate (e.g., deleting resources), the lack of per-user rate control can lead to impact amplification. Scanning with a tool like middleBrick can surface these misconfigurations by correlating authentication findings with rate-limiting checks and identifying whether limits are applied per identity. middleBrick runs unauthenticated scans that test the exposed attack surface, including whether authentication headers are required and whether rate limits vary by user identity, providing findings with severity and remediation guidance.
To illustrate a correct implementation in Axum, consider using a key extractor that incorporates the authenticated identity into the rate-limiting key. After validating Basic Auth credentials, extract the username and use it as part of the rate-limit identifier. This ensures each user has a dedicated quota, preventing a single abusive client from starving others. Below is a concise example that combines Basic Auth validation with per-user rate limiting using a tower layer and the reqwest header extraction pattern common in Axum services.
use axum::{
async_trait,
extract::Request,
http::{header, HeaderValue, StatusCode},
response::IntoResponse,
Extension,
};
use std::net::SocketAddr;
use tower_http::auth::{AuthLayer, Credentials};
use tower_http::auth::authorization::Authorization;
use tower_http::auth::BasicAuth;
use tower_http::set_response_header;
use tower_http::limit::RateLimitLayer;
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Clone)]
struct UserRateLimiter {
// Simple in-memory store: user -> count per window
limits: Arc>>, // (count, window_start)
max_requests: u32,
window_secs: u64,
}
impl UserRateLimiter {
fn new(max_requests: u32, window_secs: u64) -> Self {
Self {
limits: Arc::new(Mutex::new(HashMap::new())),
max_requests,
window_secs,
}
}
fn allow(&self, user: &str) -> bool {
let mut map = self.limits.lock().unwrap();
let now = std::time::Instant::now();
let entry = map.entry(user.to_string()).or_insert((0, now));
if now.duration_since(entry.1).as_secs() >= self.window_secs {
entry.0 = 1;
entry.1 = now;
true
} else if entry.0 < self.max_requests {
entry.0 += 1;
true
} else {
false
}
}
}
async fn validate_credentials(credentials: &BasicAuth) -> bool {
// Replace with secure credential verification (e.g., constant-time compare against a database)
credentials.user_id() == "alice" && credentials.password() == "secret"
}
async fn handler() -> impl IntoResponse {
"OK"
}
#[tokio::main]
async fn main() {
let limiter = UserRateLimiter::new(5, 60); // 5 requests per minute per user
let auth_layer = AuthLayer::new(BasicAuth::authorizer(Arc::new(validate_credentials)));
let app = axum::Router::new()
.route("/api/data", axum::routing::get(handler))
.layer(auth_layer)
.layer(RateLimitLayer::new(limiter.max_requests, std::time::Duration::from_secs(limiter.window_secs)));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Basic Auth-Specific Remediation in Axum — concrete code fixes
Remediation focuses on tying rate limits to authenticated identity and ensuring authentication failures are also rate-limited. In Axum, use a tower layer that extracts credentials, validates them, and then supplies the username to a per-user rate limiter. This prevents an attacker from exhausting a shared IP-based quota and ensures each valid user adheres to their own limit.
Use a rate-limiting structure that includes the username as part of the key. For Basic Auth, extract the user_id from the credentials after successful validation. Below is a complete, working Axum example that ties authentication and per-user rate limiting together. It uses an in-memory limiter for clarity; in production, replace with a distributed store such as Redis for multi-instance safety.
use axum::{
extract::Extension,
http::{header, StatusCode},
response::IntoResponse,
routing::get,
Router,
};
use std::net::SocketAddr;
use std::sync::Arc;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
use tower_http::auth::{AuthLayer, AuthService, Credentials};
use tower_http::auth::authorization::Authorization;
use tower_http::auth::BasicAuth;
use tower_http::limit::RateLimitLayer;
use tower_http::ServiceBuilderExt;
struct UserLimiter {
max: u32,
window: std::time::Duration,
state: Mutex>,
}
impl UserLimiter {
fn allow(&self, user: &str) -> bool {
let mut map = self.state.lock().unwrap();
let now = Instant::now();
map.entry(user.to_string())
.and_modify(|(count, window_start)| {
if now.duration_since(*window_start) >= self.window {
*count = 1;
*window_start = now;
} else if *count < self.max {
*count += 1;
}
})
.or_insert((1, now));
map.get(user).map_or(false, |(count, _)| *count <= self.max)
}
}
async fn validate_basic_auth(
credentials: BasicAuth,
Extension(limiter): Extension>,
) -> Result, (StatusCode, &'static str)> {
// Replace with secure password verification
if credentials.user_id() == "alice" && credentials.password() == "secret" && limiter.allow(credentials.user_id()) {
Ok(Authorization::new(credentials))
} else {
Err((StatusCode::UNAUTHORIZED, "invalid credentials"))
}
}
async fn data_endpoint() -> impl IntoResponse {
"sensitive data"
}
#[tokio::main]
async fn main() {
let limiter = Arc::new(UserLimiter {
max: 5,
window: std::time::Duration::from_secs(60),
state: Mutex::new(HashMap::new()),
});
let auth_service = AuthService::new(BasicAuth::authorizer(Arc::new(validate_basic_auth)));
let auth_layer = AuthLayer::new_with_service(auth_service);
let app = Router::new()
.route("/api/data", get(data_endpoint))
.layer(auth_layer)
.layer(RateLimitLayer::new(limiter.max, limiter.window))
.layer(Extension(limiter));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
In this pattern, the limiter key includes the authenticated username, ensuring that each user has a dedicated request quota. Authentication failures also consume the user-specific quota, mitigating credential-guessing attempts. For deployment, consider using a shared store (e.g., Redis via tower-redis) to synchronize limits across instances. middleBrick’s CLI can be used to verify that your endpoints enforce per-identity rate limits by running middlebrick scan <url> and reviewing the Rate Limiting and Authentication findings.