Race Condition in Actix with Basic Auth
Race Condition in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability
A race condition in Actix web when using HTTP Basic Auth occurs when concurrent requests manipulate or read shared authentication state in a way that produces an invalid or unexpected authorization outcome. In Actix, this typically arises when developer code stores per-request authentication decisions (e.g., validated credentials or derived permissions) in mutable request-local data or in application state that is accessed by multiple handlers concurrently.
Consider a handler that validates Basic Auth credentials, writes a validated user into request extensions, and then later handlers or middleware read that extension to authorize access. If validation mutates shared state (for example, a cache or a token revocation list) without proper synchronization, two requests with the same credentials may observe intermediate states. One request might see a revoked credential as valid because the revocation hasn’t been observed yet, while another sees it as invalid, leading to inconsistent authorization decisions across otherwise identical requests.
With Basic Auth, the client sends credentials on every request, but if your Actix app caches the authentication result per user to avoid repeated password checks, the cache update and reads can race. For example, a password change or an account lockout might not be immediately visible to all workers, causing some requests to succeed based on stale cached credentials while others fail. This violates the principle that authorization should be based on the current server-side state, not on a possibly stale local cache.
In a distributed Actix deployment, the race can also manifest across processes or containers if session or credential state is stored in a shared but unsynchronized cache. A login that invalidates previous sessions might not propagate instantly, allowing a request authenticated with an older token or cookie to pass if it hits a node that hasn’t refreshed its view. Because Basic Auth is typically stateless, developers sometimes introduce stateful checks (e.g., one-time nonces or per-user counters) without ensuring atomicity, which introduces the race window.
To detect this pattern, middleBrick runs parallel checks that include Authorization and BOLA/IDOR evaluations alongside Input Validation and Rate Limiting. It examines how authentication and authorization interact under concurrency, looking for endpoints where authorization depends on mutable or shared state without appropriate synchronization. By correlating spec definitions (OpenAPI/Swagger with full $ref resolution) with runtime behavior, the scanner highlights endpoints where the authorization logic could yield different outcomes when invoked concurrently, which is characteristic of a race condition in the context of Basic Auth in Actix.
Note that middleBrick reports findings with severity and remediation guidance but does not fix or block execution; it surfaces the risk so you can review your synchronization strategy and ensure that authorization is derived from authoritative, consistently synchronized state on every request.
Basic Auth-Specific Remediation in Actix — concrete code fixes
Remediation focuses on removing mutable shared state from the authorization path and ensuring each request validates credentials atomically and independently. Avoid caching validated user objects in request extensions derived from Basic Auth unless the cache is provably safe under concurrency, and prefer stateless verification on every request.
Example of an unsafe pattern that can lead to race conditions:
use actix_web::{web, HttpRequest, HttpResponse, Result};
use std::sync::Mutex;
struct AppState {
// Mutable shared cache — can cause race conditions
credential_cache: Mutex>,
}
async fn login_handler(
creds: web::Json,
data: web::Data<AppState>
) -> Result {
let mut cache = data.credential_cache.lock().unwrap();
if validate_basic(&creds.username, &creds.password) {
cache.insert(creds.username.clone(), true);
Ok(HttpResponse::Ok().finish())
} else {
Ok(HttpResponse::Unauthorized().finish())
}
}
async fn protected_handler(req: HttpRequest, data: web::Data<AppState>) -> Result {
let cache = data.credential_cache.lock().unwrap();
if let Some(valid) = cache.get(&extract_user_from_request(&req)) {
if *valid {
return Ok(HttpResponse::Ok().finish());
}
}
Ok(HttpResponse::Unauthorized().finish())
}
The race occurs because the cache is shared and mutable; concurrent login and request handlers can observe inconsistent cached values. A safer approach is to validate credentials on each request without relying on shared mutable state:
use actix_web::{web, HttpRequest, HttpResponse, Result};
struct AppState {
// Shared read-only data, e.g., user repository, does not require mutability for auth checks
user_repo: web::Data<UserRepository>,
}
async fn basic_auth_middleware(
req: HttpRequest,
payload: &mut actix_web::dev::Payload,
repo: web::Data<UserRepository>
) -> Result<(), actix_web::Error> {
if let Some((user, _)) = extract_basic_auth(&req) {
// Validate against authoritative source on every request
if repo.is_valid_credentials(&user, &extract_password_from_request(&req)?) {
req.extensions_mut().insert(user);
return Ok(());
}
}
Err(actix_web::error::ErrorUnauthorized("invalid credentials"))
}
async fn protected_handler(user: actix_web::HttpRequest) -> Result {
if let Some(user) = user.extensions().get::<User>() {
Ok(HttpResponse::Ok().body(format!("Hello, {}", user.name)))
} else {
// Should not happen if middleware enforced it
Ok(HttpResponse::Unauthorized().finish())
}
}
If you must cache, use short-lived, fine-grained entries with atomic updates and prefer read-mostly stores with compare-and-swap semantics. Ensure that any revocation or password change propagates atomically so that concurrent requests do not see stale allowed states. middleBrick’s checks for Authentication and Authorization combined with Input Validation help surface endpoints where shared state is used unsafely, enabling you to refactor toward per-request, stateless validation.