HIGH credential stuffingaxum

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.

Frequently Asked Questions

How does credential stuffing differ from brute force attacks in Axum applications?
Credential stuffing specifically uses valid username/password pairs from data breaches, while brute force attempts random combinations. Credential stuffing is more effective because it exploits password reuse across services. In Axum, both attacks target login endpoints, but credential stuffing typically involves more sophisticated automation and higher success rates due to the use of legitimate credentials.
Can middleBrick detect credential stuffing vulnerabilities in my Axum API?