HIGH password sprayingaxum

Password Spraying in Axum

How Password Spraying Manifests in Axum

Password spraying in Axum applications typically exploits weak authentication implementations that allow attackers to try common passwords across many accounts without triggering rate limits. In Axum, this often occurs when authentication middleware is improperly configured or when custom login handlers don't implement proper throttling.

A common vulnerable pattern in Axum looks like this:

async fn login(
    Extension(pool): Extension<PgPool>,
    Json(payload): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
    let conn = pool.get().await?;
    
    // Vulnerable: no rate limiting, no account lockout
    let user = sqlx::query_as(
        "SELECT * FROM users WHERE username = $1"
    )
    .bind(&payload.username)
    .fetch_one(&conn)
    .await
    .ok();
    
    if let Some(user) = user {
        if verify_password(&payload.password, &user.password_hash) {
            return Ok(Json(LoginResponse { token: generate_jwt(user) }));
        }
    }
    
    Err(StatusCode::UNAUTHORIZED)
}

This handler is vulnerable because it responds identically whether the username exists or not, but more critically, it doesn't implement any rate limiting. An attacker can send thousands of requests with common passwords like "password123" across different usernames without detection.

The attack manifests through timing analysis. When a username doesn't exist, the database query fails immediately. When it does exist but the password is wrong, there's a slight delay. Attackers can exploit this to enumerate valid usernames before launching the spraying attack.

Another Axum-specific vulnerability occurs when using extractors that don't properly validate request volume:

async fn vulnerable_login(
    Json(payload): Json<LoginRequest>,
    State<AppState>: State<AppState>,
) -> Result<Json<LoginResponse>, StatusCode> {
    // No rate limiting on the endpoint
    // Attacker can send unlimited requests
    process_login(&payload).await
}

Without middleware to limit requests per IP or per account, Axum applications become susceptible to credential stuffing at scale.

Axum-Specific Detection

Detecting password spraying in Axum requires monitoring both application logs and network traffic patterns. The most effective approach combines middleware-based monitoring with external scanning tools like middleBrick.

Implement middleware to track authentication attempts:

use axum_extra::rate_limit::RateLimiter;
use std::time::Duration;

async fn auth_middleware(
    Extension(rate_limiter): Extension<RateLimiter>,
    Json(payload): Json<LoginRequest>,
    Extension(db): Extension<PgPool>,
) -> Result<Json<LoginResponse>, StatusCode> {
    // Track attempts per IP and per username
    let ip = extract_ip().await?;
    let attempts = rate_limiter.get(&ip).await;
    
    if attempts > 5 {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }
    
    // Log all authentication attempts for analysis
    sqlx::query(
        "INSERT INTO auth_attempts (ip, username, success, timestamp) VALUES ($1, $2, $3, NOW())"
    )
    .bind(&ip)
    .bind(&payload.username)
    .bind(false)
    .execute(&db)
    .await?;
    
    // Continue with authentication
    process_login(&payload).await
}

middleBrick's black-box scanning approach is particularly effective for detecting password spraying vulnerabilities because it tests the unauthenticated attack surface without requiring credentials. The scanner sends multiple authentication attempts with common passwords and analyzes the responses for timing differences and error patterns.

For OpenAPI-based Axum applications, middleBrick can analyze your spec files to identify endpoints that lack proper authentication controls. The tool examines your axum::routing::post and axum::routing::get definitions to find authentication endpoints that might be vulnerable.

Key detection indicators include:

  • Consistent response times regardless of credential validity
  • Lack of account lockout mechanisms
  • No IP-based rate limiting on authentication endpoints
  • Detailed error messages that reveal whether usernames exist
  • Missing multi-factor authentication requirements

Axum-Specific Remediation

Securing Axum applications against password spraying requires implementing proper rate limiting, account lockout mechanisms, and authentication best practices. Here's how to implement these protections using Axum's native features.

First, implement IP-based rate limiting using axum_extra:

use axum_extra::rate_limit::RateLimiter;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/login", post(login_with_rate_limit))
        .layer(Extension(RateLimiter::new(10, Duration::from_secs(60))));
    
    axum::Server::bind(&[([127, 0, 0, 1], 3000)])
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn login_with_rate_limit(
    Extension(rate_limiter): Extension<RateLimiter>,
    Json(payload): Json<LoginRequest>,
    Extension(db): Extension<PgPool>,
) -> Result<Json<LoginResponse>, StatusCode> {
    let ip = extract_ip().await?;
    
    // Check rate limit before processing
    if rate_limiter.check(&ip).await.is_some() {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }
    
    // Check account-specific rate limiting
    let account_attempts = get_auth_attempts(&db, &payload.username).await?;
    if account_attempts > 3 {
        return Err(StatusCode::TOO_MANY_REQUESTS);
    }
    
    // Process login
    let result = verify_credentials(&db, &payload).await;
    
    // Log attempt regardless of success
    log_auth_attempt(&db, &payload.username, result.is_ok()).await;
    
    result
}

Implement account lockout after failed attempts:

async fn verify_credentials(
    db: &PgPool,
    payload: &LoginRequest,
) -&; Result<LoginResponse, StatusCode> {
    let conn = db.get().await?;
    
    // Check if account is locked
    let locked = sqlx::query_as(
        "SELECT locked_until FROM users WHERE username = $1"
    )
    .bind(&payload.username)
    .fetch_one(&conn)
    .await
    .optional()?;
    
    if let Some(locked) = locked {
        if locked.locked_until > chrono::Utc::now().naive_utc() {
            return Err(StatusCode::LOCKED_OUT);
        }
    }
    
    // Verify password
    let user = sqlx::query_as(
        "SELECT * FROM users WHERE username = $1"
    )
    .bind(&payload.username)
    .fetch_one(&conn)
    .await
    .ok();
    
    if let Some(user) = user {
        if verify_password(&payload.password, &user.password_hash) {
            // Reset failed attempts on success
            reset_failed_attempts(db, &payload.username).await?;
            return Ok(LoginResponse { token: generate_jwt(user) });
        } else {
            // Increment failed attempts
            increment_failed_attempts(db, &payload.username).await?;
            
            // Lock account after 5 failed attempts
            if get_failed_attempts(db, &payload.username).await? >= 5 {
                lock_account(db, &payload.username).await?;
            }
        }
    }
    
    Err(StatusCode::UNAUTHORIZED)
}

Always use constant-time comparison and uniform error responses:

use subtle::ConstantTimeEq;

fn verify_password(hashed: &str, provided: &str) -> bool {
    // Use constant-time comparison to prevent timing attacks
    let expected_hash = bcrypt::hash(provided, 12).unwrap();
    let result = expected_hash.as_bytes().ct_eq(hashed.as_bytes());
    result.unwrap_u8() == 1
}

Finally, integrate middleBrick into your development workflow to catch these issues early:

# Install middleBrick CLI
npm install -g middlebrick

# Scan your API before deployment
middlebrick scan https://api.yourdomain.com/login

# Add to CI/CD pipeline
# .github/workflows/security.yml
name: Security Scan
on: [push]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run middleBrick scan
      run: middlebrick scan ${{ secrets.API_URL }} --fail-below B

These mitigations work together to prevent password spraying by limiting the volume of attempts, locking compromised accounts, and ensuring uniform response times that don't leak information to attackers.

Frequently Asked Questions

How does password spraying differ from brute force attacks in Axum applications?
Password spraying attacks try common passwords across many accounts (low volume, high breadth), while brute force attacks try many passwords against a single account (high volume, low breadth). In Axum, password spraying is particularly dangerous because it can bypass basic rate limiting that only tracks attempts per endpoint. A robust defense requires both IP-based rate limiting and account-specific tracking to catch both attack patterns.
Can middleBrick detect password spraying vulnerabilities in my Axum API?
Yes, middleBrick's black-box scanning approach specifically tests for password spraying vulnerabilities by sending multiple authentication attempts with common passwords and analyzing response patterns. The scanner looks for timing inconsistencies, lack of rate limiting, and uniform error responses that could indicate vulnerability. It tests the unauthenticated attack surface without requiring credentials, making it ideal for identifying these issues before deployment.