HIGH brute force attackactixbasic auth

Brute Force Attack in Actix with Basic Auth

Brute Force Attack in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability

A brute force attack against an Actix web service that uses HTTP Basic Authentication relies on repeated credential guesses to discover a valid username and password pair. Because Basic Auth sends credentials in an easily decoded base64 string (not inherently protected without transport encryption), each request reveals the authorization header format, enabling automated guessing. When no protective mechanisms are in place, an attacker can systematically iterate over common passwords or use credential lists, sending many requests to the same authentication endpoint.

Actix web applications that expose an unauthenticated or improperly rate-limited endpoint accepting Basic Auth credentials become susceptible to this attack vector. Without mechanisms such as account lockout, incremental delays, or IP-based rate limiting, each request provides immediate feedback—typically an HTTP 401 Unauthorized for invalid credentials and 200 OK for valid credentials. This clear differentiation allows an attacker to validate guesses efficiently. The unauthenticated attack surface scanned by middleBrick can detect missing rate limiting and weak account policies that facilitate brute force attempts.

Moreover, Basic Auth does not inherently bind credentials to a session or include built‑in replay protection, so captured authentication tokens can be reused until expiration. When combined with predictable usernames (e.g., admin, user) and weak password policies, the attack surface expands. Security checks such as Rate Limiting and Authentication implemented by middleBrick identify whether the service responds differently to rapid successive requests and whether the application provides clear indicators that assist an attacker in refining guesses.

Basic Auth-Specific Remediation in Actix — concrete code fixes

Remediation focuses on reducing the effectiveness of credential guessing by enforcing strict rate limits, introducing delays, and avoiding information leakage in authentication responses. Implement server‑side throttling so that repeated failed attempts from a single IP or user are progressively slowed or temporarily blocked. Always use HTTPS to protect the base64‑encoded credentials in transit, and avoid exposing whether a username exists independently of password validity by using a consistent 401 response.

Below are concrete Actix examples that combine middleware and extractor logic to enforce these protections. The first example shows a guarded login handler that checks a simple in‑memory rate limit before validating credentials. The second demonstrates a more structured approach using a custom guard and middleware to centralize rate limiting across all authenticated routes.

Example 1: Per‑route rate limiting with delay on failure

use actix_web::{web, App, HttpResponse, HttpServer, Responder, guard, middleware, http::header};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

struct LoginState {
    attempts: Mutex>>,
    max_attempts: usize,
    block_duration: Duration,
}

async fn basic_auth_login(
    credentials: web::Bytes,
    state: web::Data>,
) -> impl Responder {
    let header_str = match std::str::from_utf8(&credentials) {
        Ok(s) => s,
        Err(_) => return HttpResponse::Unauthorized().finish(),
    };
    if !header_str.starts_with("Basic ") {
        return HttpResponse::Unauthorized().finish();
    }
    let token = &header_str[6..];
    let decoded = match base64::decode(token) {
        Ok(d) => d,
        Err(_) => return HttpResponse::Unauthorized().finish(),
    };
    let parts: Vec<&str> = std::str::from_utf8(&decoded).map_err(|_| ())
        .and_then(|s| s.splitn(2, ':').collect::>().try_into())
        .map(|[user, pass]| (user, pass))
        .map_err(|_| ())?;
    let (user, pass) = parts;

    let ip = // obtain peer addr via connection info; placeholder
    let key = ip.to_string();
    let mut attempts = state.attempts.lock().unwrap();
    let now = Instant::now();
    let user_attempts = attempts.entry(key.clone()).or_default();
    user_attempts.retain(|t| now.duration_since(*t) < Duration::from_secs(300));
    if user_attempts.len() >= state.max_attempts {
        if now.duration_since(user_attempts[0]) < state.block_duration {
            return HttpResponse::TooManyRequests().finish();
        } else {
            user_attempts.clear();
        }
    }
    user_attempts.push(now);

    // Constant‑time comparison to avoid timing leaks
    let valid = slow_equals(user.as_bytes(), b"admin") && slow_equals(pass.as_bytes(), b"s3cur3P@ss");
    if valid {
        HttpResponse::Ok().body("Authenticated")
    } else {
        // Artificial delay to deter online guessing
        tokio::time::sleep(Duration::from_millis(500)).await;
        HttpResponse::Unauthorized().finish()
    }
}

fn slow_equals(a: &[u8], b: &[u8]) -> bool {
    use subtle::ConstantTimeEq;
    a.ct_eq(b).into()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = Arc::new(LoginState {
        attempts: Mutex::new(HashMap::new()),
        max_attempts: 5,
        block_duration: Duration::from_secs(30),
    });
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(state.clone()))
            .service(
                web::resource("/login")
                    .guard(guard::Get())
                    .route(web::post().to(basic_auth_login)),
            )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Example 2: Centralized middleware for rate limiting

use actix_web::{dev::ServiceRequest, Error, middleware, web, App, HttpResponse, HttpServer, Responder};
use actix_web::body::BoxBody;
use actix_web::http::StatusCode;
use std::future::{ready, Ready};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

struct RateLimiter {
    attempts: Mutex>>,
    max: usize,
    window: Duration,
    delay: Duration,
}

impl RateLimiter {
    fn new(max: usize, window: Duration, delay: Duration) -> Arc {
        Arc::new(Self {
            attempts: Mutex::new(HashMap::new()),
            max,
            window,
            delay,
        })
    }
}

impl middleware::Transform for RateLimiter
where
    S: middleware::Service, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = actix_web::dev::ServiceResponse;
    type Error = Error;
    type Transform = RateLimiterMiddleware;
    type InitError = ();
    type Future = Ready>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(RateLimiterMiddleware { service, limiter: self.clone() }))
    }
}

struct RateLimiterMiddleware {
    service: S,
    limiter: Arc,
}

impl middleware::Service for RateLimiterMiddleware
where
    S: middleware::Service, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = actix_web::dev::ServiceResponse;
    type Error = Error;
    type Future = Pin> + 'static>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> {
        self.service.poll_ready(cx)
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        let peer_addr = req.connection_info().realip_remote_addr().unwrap_or("unknown");
        let mut attempts = self.limiter.attempts.lock().unwrap();
        let now = Instant::now();
        let list = attempts.entry(peer_addr.to_string()).or_default();
        list.retain(|t| now.duration_since(*t) < self.limiter.window);
        if list.len() >= self.limiter.max {
            if now.duration_since(list[0]) < self.limiter.delay {
                return Box::pin(async { Err(actix_web::error::ErrorTooManyRequests("Too many attempts")) });
            } else {
                list.clear();
            }
        }
        list.push(now);
        let fut = self.service.call(req);
        Box::pin(async move {
            let res = fut.await?;
            Ok(res.map_into_right_body())
        })
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let limiter = RateLimiter::new(5, Duration::from_secs(60), Duration::from_millis(500));
    HttpServer::new(move || {
        App::new()
            .wrap(limiter.clone())
            .default_service(web::route().to(|| async { HttpResponse::Ok().body("ok") }))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

These patterns reduce online brute force effectiveness by limiting request frequency and adding small delays on failure, while maintaining consistent error handling to avoid user enumeration. middleBrick scans can verify whether such protections are present by testing rate limiting and authentication behavior across the unauthenticated attack surface.

Frequently Asked Questions

How does Basic Auth over HTTP enable brute force attacks even when passwords are strong?
Basic Auth encodes credentials in base64, which is trivial to decode. Without HTTPS, intercepted traffic reveals the credentials directly. Additionally, if the server responds with distinct status codes (e.g., 401 vs 200) for wrong vs correct passwords, attackers can perform online guessing regardless of password strength. Using HTTPS is necessary but not sufficient; rate limiting and consistent error responses are required to mitigate brute force risk.
Why is a constant‑time comparison important in the Actix Basic Auth example?
A standard string comparison short‑circuits on the first mismatching byte, which leaks timing information about how much of the password matched. An attacker can use timing differences to iteratively recover the correct password character by character. Using a constant‑time comparison (e.g., subtle::ConstantTimeEq) ensures the server takes the same amount of time regardless of how many characters match, preventing timing‑based side‑channel attacks.