Credential Stuffing in Axum
How Credential Stuffing Manifests in Axum
Credential stuffing attacks target Axum applications by exploiting login endpoints that lack proper rate limiting and authentication controls. In Axum, these attacks typically manifest through brute-force attempts on login handlers, where attackers use automated tools to submit large volumes of username/password combinations harvested from data breaches.
The most common attack pattern involves repeatedly calling the login handler with different credential pairs. In Axum, this often looks like:
async fn login(
Query(query): Query<LoginQuery>,
data: Data<AppState>,
) -> Result<Redirect, StatusCode> {
let user = data.users.find_by_username(&query.username).await;
match user {
Some(user) if verify_password(&query.password, &user.password_hash) => {
// Successful login
Ok(Redirect::to("/dashboard"))
}
_ => {
// No delay, no rate limiting
Err(StatusCode::UNAUTHORIZED)
}
}
}The vulnerability here is that every failed attempt returns immediately without any throttling mechanism. Attackers can send thousands of requests per minute, systematically testing credential combinations. Without proper controls, Axum's async nature actually enables these attacks to scale horizontally across multiple concurrent connections.
Another manifestation occurs in API endpoints that expose user enumeration vulnerabilities. Attackers can determine valid usernames by observing response timing or error messages:
async fn get_user(
Path(username): Path<String>,
data: Data<AppState>,
) -> Result<Json<User>, StatusCode> {
let user = data.users.find_by_username(&username).await;
match user {
Some(user) => Ok(Json(user)),
None => Err(StatusCode::NOT_FOUND) // Reveals valid usernames via enumeration
}
}Attackers combine username enumeration with credential stuffing by first harvesting valid accounts, then targeting login endpoints with those specific usernames and common passwords.
Axum-Specific Detection
Detecting credential stuffing in Axum applications requires monitoring both application logs and network traffic patterns. The most effective approach combines middleware-based detection with specialized security scanning tools.
Implement request counting middleware to track authentication attempts:
use axum_extra::rate_limit::RateLimitStore;
use axum_extra::rate_limit::InMemoryStore;
use axum_extra::rate_limit::rate_limit;
async fn login(
Query(query): Query<LoginQuery>,
State(store): State<InMemoryStore>,
Extension(config): Extension<RateLimitConfig>,
) -> Result<Redirect, StatusCode> {
// Rate limiting automatically applied
Ok(Redirect::to("/dashboard"))
}
let app = Router::new()
.route("/login", post(login).layer(rate_limit(5, Duration::from_minutes(1))))
.with_state(store);middleBrick's credential stuffing detection specifically identifies these vulnerabilities in Axum applications by testing login endpoints with varying credential combinations and measuring response times and rate limiting effectiveness. The scanner analyzes the OpenAPI spec to understand authentication flows and then actively probes endpoints to detect missing protections.
Key detection indicators include:
- Consistent response times regardless of authentication success/failure
- Missing or ineffective rate limiting on login endpoints
- Detailed error messages that reveal valid usernames
- Lack of account lockout mechanisms after multiple failures
- Missing CAPTCHA or other human verification challenges
middleBrick's black-box scanning approach tests these characteristics without requiring source code access, making it ideal for production deployments where you need to verify deployed security controls.
Axum-Specific Remediation
Remediating credential stuffing in Axum requires implementing multiple defensive layers. The most effective approach combines rate limiting, account lockout, and progressive delays.
Implement comprehensive rate limiting using axum_extra's built-in middleware:
use axum_extra::rate_limit::rate_limit;
use axum_extra::rate_limit::InMemoryStore;
use axum_extra::rate_limit::RateLimitStore;
let store = InMemoryStore::new();
let app = Router::new()
.route("/login", post(login).layer(rate_limit(10, Duration::from_minutes(5))))
.with_state(store)This limits each IP address to 10 login attempts per 5 minutes. For production applications, consider using Redis-backed stores for distributed rate limiting across multiple instances.
Implement progressive delays to slow down automated attacks:
async fn login(
Query(query): Query<LoginQuery>,
data: Data<AppState>,
Extension(attempt_tracker): Extension<AttemptTracker>,
) -> Result<Redirect, StatusCode> {
let attempts = attempt_tracker.increment_attempts(&query.username).await;
if attempts > 3 {
// Progressive delay: 1s, 2s, 4s, etc.
tokio::time::sleep(Duration::from_secs(1u64 << (attempts - 3))).await;
}
// Authentication logic...
}Account lockout mechanisms provide another defensive layer:
async fn login(
Query(query): Query<LoginQuery>,
data: Data<AppState>,
Extension(attempt_tracker): Extension<AttemptTracker>,
) -> Result<Redirect, StatusCode> {
let attempts = attempt_tracker.increment_attempts(&query.username).await;
if attempts > 10 {
// Lock account for 15 minutes
attempt_tracker.lock_account(&query.username).await;
return Err(StatusCode::TOO_MANY_REQUESTS);
}
// Authentication logic...
}For API endpoints, implement proper error handling that doesn't reveal account existence:
async fn get_user(
Path(username): Path<String>,
data: Data<AppState>,
) -> Result<Json<User>, StatusCode> {
let user = data.users.find_by_username(&username).await;
match user {
Some(user) => Ok(Json(user)),
None => Err(StatusCode::NOT_FOUND) // Consider returning generic error
}
}Consider implementing CAPTCHA challenges after threshold attempts:
async fn login(
Query(query): Query<LoginQuery>,
data: Data<AppState>,
Extension(attempt_tracker): Extension<AttemptTracker>,
) -> Result<Redirect, StatusCode> {
let attempts = attempt_tracker.increment_attempts(&query.username).await;
if attempts > 5 {
// Require CAPTCHA verification
if !verify_captcha(&query.captcha_response).await? {
return Err(StatusCode::BAD_REQUEST);
}
}
// Authentication logic...
}middleBrick's scanning can verify these implementations by testing the actual deployed behavior, ensuring your protections work as intended in production.