HIGH axumpassword spraying

Password Spraying in Axum

How Password Spraying Manifests in Axum

Password spraying is a credential stuffing variant where attackers try a few common passwords (e.g., Password123, admin) across many usernames to avoid account lockouts. In Axum applications, this attack exploits two common patterns: non-rate-limited authentication endpoints and inconsistent error responses that enable user enumeration.

Axum handlers often accept credentials via extractors like axum::extract::Json<LoginRequest> or axum::extract::Form<LoginRequest>. A vulnerable handler might look like this:

use axum::{extract::Json, response::IntoResponse, routing::post, Router};ntt#[derive(Deserialize)]nttstruct LoginRequest {ntt    username: String,ntt    password: String,ntt}nttnttasync fn login(Json(payload): Json) -> impl IntoResponse {ntt    if payload.username == "admin" && payload.password == "secret" {ntt        // Success response (e.g., JWT token)ntt        return (StatusCode::OK, "{ \"token\": \"xyz\" }").into_response();ntt    }ntt    // Generic error message? Or specific?ntt    (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response()ntt}nttnttlet app = Router::new().route("/login", post(login));

The vulnerability arises if:

  • No rate limiting is applied to the /login route. Axum does not include built-in rate limiting; developers must add middleware like tower::limit::rate or use crates such as axum-rate-limiter. Without it, an attacker can submit hundreds of password guesses per minute.
  • Error messages differ for valid vs. invalid usernames. For example, returning {"error":"User not found"} for unknown usernames but {"error":"Invalid password"} for known ones allows attackers to enumerate valid accounts before spraying.

Additionally, Axum applications using stateful session authentication (e.g., via axum::extract::Session) may expose session fixation risks if session IDs are not regenerated on login. While not directly password spraying, weak session handling can amplify the impact of compromised credentials.

Attackers automate spraying with tools like ffuf or custom scripts, targeting the login endpoint with a wordlist of usernames and a small set of common passwords. The lack of per-username or per-IP throttling in Axum routes makes this feasible.

Axum-Specific Detection

Detecting password spraying vulnerabilities in Axum requires testing the authentication endpoint's behavior under repeated, varied credential attempts. middleBrick's unauthenticated black-box scan performs this by:

  • Probing the login endpoint with sequential requests using common passwords (e.g., password, 123456) against a list of likely usernames (derived from the API's OpenAPI spec or common patterns).
  • Analyzing response consistency: If the status code, response body, or latency differs between requests with valid usernames/invalid passwords vs. invalid usernames, it indicates user enumeration (OWASP API:2).
  • Checking for rate limiting headers (Retry-After, X-RateLimit-*) or HTTP 429 responses after a threshold. Absence of these after 10–20 rapid requests suggests no rate limiting (OWASP API:4).
  • Correlating with OpenAPI spec: If the spec defines a /login operation with 200 and 401 responses but no security scheme, middleBrick flags missing authentication enforcement.

For example, a scan against an Axum app might reveal:

TestExpected Secure BehaviorVulnerable Observation
User enumerationSame 401 response for any invalid credential pair401 {"error":"Invalid password"} for known user vs. 404 {"error":"User not found"} for unknown
Rate limitingHTTP 429 after ~5–10 failed attempts per IP/usernameAll 401 responses, no 429, no Retry-After header

middleBrick's Authentication and Rate Limiting checks (among its 12 parallel tests) capture these patterns. The scan takes 5–15 seconds, requiring no credentials or config—just the API URL. The resulting report assigns a risk score (A–F) and highlights findings with OWASP API Top 10 mapping (e.g., API2:2023 — Broken Authentication). For LLM/AI endpoints in Axum (e.g., /chat/completions), middleBrick also tests prompt injection and agency, but password spraying focuses on traditional auth endpoints.

To manually verify, use curl to send rapid requests and observe responses:

for i in {1..20}; do curl -X POST http://api.example.com/login -H "Content-Type: application/json" -d '{"username":"admin","password":"wrong"}'; done

If all return 401 without delay or headers, rate limiting is absent.

Axum-Specific Remediation

Remediate password spraying in Axum by implementing per-username and per-IP rate limiting and consistent error responses. Axum's middleware ecosystem provides tools:

1. Apply rate limiting middleware
Use tower::limit::rate or a dedicated crate. Example with axum-rate-limiter:

use axum::{routing::post, Router};nttuse axum_rate_limiter::{RateLimiter, RateLimiterLayer};nttuse std::num::NonZeroU32;nttntt// Limit: 5 attempts per 60 seconds per IP address (keyed by peer IP)nttlet rate_limiter = RateLimiter::new(ntt    NonZeroU32::new(5).unwrap(), // max requestsntt    std::time::Duration::from_secs(60), // per periodntt    axum_rate_limiter::key::IpKey, // rate limit by client IPntt);nttnttlet app = Router::new()ntt    .route("/login", post(login))ntt    .layer(RateLimiterLayer::new(rate_limiter));

For per-username limits (more effective against spraying), use a custom key that includes the username (after validation to avoid DoS):

use axum::extract::Path;nttuse axum_rate_limiter::{RateLimiter, RateLimiterLayer, key::CustomKey};nttnttstruct LoginKey {ntt    username: String,ntt}nttnttimpl CustomKey for LoginKey {ntt    fn compute(&self) -> String {ntt        format!("login:{}", self.username.to_ascii_lowercase())ntt    }ntt}nttntt// In handler, extract username and apply layer conditionally?ntt// Better: apply a global layer but use a key that combines IP + username?ntt// However, rate limiter layers are applied before handler extraction.ntt// Alternative: use a middleware that wraps the login route specifically.

Since Axum middleware runs before extractors, a more flexible approach is a custom middleware that checks the request path and body:

use axum::{body::Body, http::Request, middleware::Next, response::Response};nttuse std::sync::Arc;nttuse tokio::sync::RwLock;nttuse std::collections::HashMap;nttuse std::time::{Duration, Instant};nttnttstruct LoginAttempts {ntt    // key: (ip, username_lower) -> (count, reset_time)ntt    attempts: RwLock>,ntt}nttnttimpl LoginAttempts {ntt    async fn is_allowed(&self, ip: &str, username: &str) -> bool {ntt        let key = (ip.to_string(), username.to_ascii_lowercase());ntt        let mut attempts = self.attempts.write().await;ntt        let now = Instant::now();ntt        if let Some((count, reset)) = attempts.get(&key) {ntt            if now < *reset && *count >= 5 {ntt                return false;ntt            }ntt        }ntt        // Reset or incrementntt        let count = attempts.entry(key).or_insert((0, now + Duration::from_secs(60))).0;ntt        attempts.insert(key, (count + 1, reset));ntt        truentt    }ntt}nttnttasync fn rate_limit_middleware(ntt    req: Request,ntt    next: Next,ntt    attempts: Arc,ntt) -> Response {ntt    if req.uri().path() == "/login" && req.method() == "POST" {ntt        // Extract IP and username from body (requires buffering)ntt        // Note: this is simplified; in production, use extractors earlier or limit by IP only.ntt        let ip = req.extensions().get::()ntt            .map(|a| a.ip().to_string())ntt            .unwrap_or_else(|| "unknown".to_string());ntt        // Cannot easily extract body here without consuming; consider using a filterntt        // For demo: apply per-IP rate limit only at this stage.ntt        if !attempts.is_allowed(&ip, "").await {ntt            return (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded").into_response();ntt        }ntt    }ntt    next.run(req).awaitntt}

2. Normalize error responses
Ensure the login handler returns identical HTTP status codes, body structure, and timing for all authentication failures. In Axum:

use axum::{http::StatusCode, response::Json};nttuse serde_json::json;nttnttasync fn login(Json(payload): Json) -> impl IntoResponse {ntt    // Always perform user lookup (constant-time check optional but recommended)ntt    let user_exists = check_user_exists(&payload.username).await;ntt    let password_valid = if user_exists {ntt        verify_password(&payload.password, get_stored_hash(&payload.username)).awaitntt    } else {ntt        false // Still run password hash comparison to avoid timing leaks?ntt    };nttntt    if password_valid {ntt        // Generate session/JWT...ntt        (StatusCode::OK, Json(json!({ "token": "..." }))).into_response()ntt    } else {ntt        // Identical response regardless of whether username existsntt        (StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid credentials" }))).into_response()ntt    }ntt}

3. Additional hardening:

  • Use bcrypt or argon2 for password hashing (via password-hash crate).
  • Regenerate session IDs on login (axum::extract::Session with tower-sessions).
  • Consider multi-factor authentication for sensitive APIs.

After deploying fixes, rescan with middleBrick (via CLI: middlebrick scan https://api.example.com or GitHub Action in CI/CD) to verify the Authentication and Rate Limiting findings are resolved. The Pro plan's continuous monitoring can alert on regressions.

Frequently Asked Questions

How does middleBrick test for password spraying without valid credentials?
middleBrick performs unauthenticated black-box testing. For password spraying, it sends sequential login attempts with common passwords (e.g., 'password', '123456') against usernames inferred from the API's OpenAPI spec (e.g., 'admin', 'test') or common patterns. It observes response consistency (status codes, body messages) and checks for rate limiting headers or HTTP 429 responses. No real credentials are used; the scan analyzes the endpoint's behavior under repeated failure conditions.
Does Axum have built-in rate limiting for authentication endpoints?
No. Axum is a minimal framework and does not include built-in rate limiting. Developers must add it via middleware, such as using the 'tower' crate's 'RateLimit' layer or third-party crates like 'axum-rate-limiter'. Effective protection requires configuring per-IP or per-username limits specifically on authentication routes like '/login'.