Password Spraying in Actix with Basic Auth
Password Spraying in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack where an adversary uses a small number of common passwords against many accounts. When an Actix web service uses HTTP Basic Auth without additional protections, the attack surface aligns poorly with this technique because the credentials are transmitted on every request and the server-side logic often lacks per-account rate limiting.
In Actix, if you protect endpoints with Basic Auth middleware but do not enforce account-level throttling or lockout, a spray attacker can iterate through a list of weak passwords (for example, Password1, Welcome1, Company2024) across many usernames. Because each request is independent and the server typically returns a 401 for any invalid credential pair, the attacker can infer valid usernames only when the server response differs (e.g., timing, message text). Even when responses are uniform, the absence of rate limiting at the user or IP level allows the attacker to make many attempts within the 5–15 second scan window that middleBrick uses, increasing the likelihood of success against weak passwords.
Moreover, because middleBrick tests unauthenticated attack surfaces, it can detect whether Actix endpoints accepting Basic Auth are missing supplemental controls such as incremental delays after failures, account lockouts, or captchas. The scanner’s Authentication check flags endpoints where a spray-like pattern could be attempted without triggering defenses. Meanwhile, the Rate Limiting check evaluates whether the API limits requests per identity or IP, and the Input Validation check ensures that username and password parsing does not expose additional information that could aid an attacker. An absence of these controls elevates the risk score and produces findings mapped to OWASP API Top 10:2023 — API1:2023 Broken Object Level Authorization and API2:2023 Broken Authentication.
Real-world attack patterns resemble the use of common passwords listed in known breaches, paired with enumeration of valid usernames via timing differences or response messages. If an Actix application also exposes account metadata in error responses, an attacker can refine a spray campaign before middleBrick even runs. The scanner’s unauthenticated approach means it does not need credentials to surface these issues; it probes the endpoints as an external attacker would during reconnaissance.
Basic Auth-Specific Remediation in Actix — concrete code fixes
Remediation focuses on reducing the effectiveness of password spraying by combining strong credential handling with strict rate controls and uniform responses. Below are concrete, syntactically correct examples for Actix-web in Rust that implement these measures.
Example 1: Basic Auth with constant-time failure responses and per-account rate limiting
use actix_web::{web, App, HttpResponse, HttpServer, middleware::Logger};
use actix_web::http::header::HeaderValue;
use actix_web::dev::ServiceRequest;
use actix_web::Error;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
// Simple in-memory rate store: username -> Vec
struct RateStore(Arc>>>);
impl RateStore {
fn new() -> Self {
RateStore(Arc::new(Mutex::new(HashMap::new())))
}
fn is_allowed(&self, username: &str, max_attempts: usize, window_secs: u64) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut store = self.0.lock().unwrap();
let timestamps = store.entry(username.to_string()).or_default();
// Keep only recent attempts
timestamps.retain(|&t| now - t < window_secs);
if timestamps.len() >= max_attempts {
return false;
}
timestamps.push(now);
true
}
}
async fn auth_middleware(
req: ServiceRequest,
credentials: (String, String), // (username, password)
rate_store: web::Data<RateStore>
) -> Result<ServiceRequest, Error> {
let (username, password) = credentials;
// Enforce rate limit per username before checking password
if !rate_store.is_allowed(&username, 5, 60) {
// Return uniform 401 to avoid leaking account status
return Err(actix_web::error::ErrorUnauthorized("Unauthorized"));
}
// In real code, use a constant-time comparison against a hashed/stored secret
let valid = check_password_hash_const_time(&username, &password).await;
if valid {
Ok(req)
} else {
// Uniform response regardless of username existence
Err(actix_web::error::ErrorUnauthorized("Unauthorized"))
}
}
async fn check_password_hash_const_time(username: &str, password: &str) -> bool {
// Placeholder: replace with proper password hashing verification
// Ensure this operation takes constant time for a given username length
password == "correct_hashed_password_placeholder"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let rate_store = web::Data::new(RateStore::new());
HttpServer::new(move || {
App::new()
.app_data(rate_store.clone())
.wrap(Logger::default())
.route("/secure", actix_web::web::get().to(|| async { "OK" }))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Example 2: Middleware that enforces global and per-IP rate limits
use actix_web::{web, App, HttpResponse, HttpServer, dev::ServiceRequest, Error};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
struct IpRateStore(Arc<Mutex<HashMap<String, Vec<u64>>>>);
impl IpRateStore {
fn new() -> Self {
IpRateStore(Arc::new(Mutex::new(HashMap::new())))
}
fn is_allowed_ip(&self, ip: &str, max: usize, window: u64) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut store = self.0.lock().unwrap();
let timestamps = store.entry(ip.to_string()).or_default();
timestamps.retain(|&t| now - t < window);
if timestamps.len() >= max {
return false;
}
timestamps.push(now);
true
}
}
async fn require_auth_and_rate_limit(
req: ServiceRequest,
ip_rate: web::Data<IpRateStore>
) -> Result<ServiceRequest, Error> {
let peer_addr = req.peer_addr().map(|a| a.ip().to_string()).unwrap_or_else(|| "unknown".into());
if !ip_rate.is_allowed_ip(&peer_addr, 30, 60) {
return Err(actix_web::error::ErrorTooManyRequests("Rate limit exceeded"));
}
// Extract and validate Basic Auth header
let credentials = match req.headers().get("Authorization") {
Some(hv) => {
let header = hv.to_str().unwrap_or("");
if header.starts_with("Basic ") {
let data = general_purpose::STANDARD.decode(&header[6..]).unwrap_or_default();
if let Ok(s) = String::from_utf8(data) {
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() == 2 {
(parts[0].to_string(), parts[1].to_string())
} else {
(String::new(), String::new())
}
} else {
(String::new(), String::new())
}
} else {
(String::new(), String::new())
}
}
None => (String::new(), String::new()),
};
if credentials.0.is_empty() || credentials.1.is_empty() {
return Err(actix_web::error::ErrorUnauthorized("Missing auth"));
}
// Constant-time validation placeholder
if check_password_hash_const_time(&credentials.0, &credentials.1).await {
Ok(req)
} else {
Err(actix_web::error::ErrorUnauthorized("Unauthorized"))
}
}
async fn check_password_hash_const_time(_username: &str, _password: &str) -> bool {
// Replace with secure password verification
false
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let ip_rate = web::Data::new(IpRateStore::new());
HttpServer::new(move || {
App::new()
.app_data(ip_rate.clone())
.wrap(Logger::default())
.route("/api/protected", actix_web::web::get().to(|| async { "Secret data" }))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
These examples show how to combine uniform error responses with per-username and per-IP rate limiting to make password spraying significantly harder. middleBrick’s scans will highlight endpoints missing such protections and map findings to frameworks like OWASP API Top 10 and NIST guidance, helping you prioritize fixes.
For teams using the middleBrick CLI, you can run scans from the terminal with middlebrick scan <url> to validate these controls. The Pro plan adds continuous monitoring and CI/CD integration, so future changes to authentication or rate limiting can be automatically validated before deployment.