Information Disclosure in Actix with Basic Auth
Information Disclosure in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability
Information Disclosure occurs when an API returns sensitive data to an entity that should not have access to it. In Actix web applications, combining HTTP Basic Authentication with insecure implementation patterns can unintentionally expose credentials, application metadata, or user data.
Basic Authentication encodes a username:password pair in Base64 and transmits it in the Authorization header. Because Base64 is reversible and not encrypted, any party who can observe the request (e.g., via logs, proxies, or insecure transport) can recover the credentials. In Actix, if routes that expect Basic Auth are not consistently guarded, or if error messages reveal whether a header was present or malformed, an attacker may infer valid usernames or enumerate accounts. For example, differing responses between “missing header” and “invalid credentials” can leak information that assists further attacks.
When route handlers do not properly validate authorization before accessing or serializing data, they might return representations that include sensitive fields such as internal IDs, email addresses, or role mappings. In an Actix service that uses extractor-based guards, failing to restrict data exposure in serialization logic can result in responses that include more information than intended. Consider an endpoint that returns a user profile; if authorization checks are bypassed or incorrectly composed, a caller might receive another user’s data simply by guessing identifiers, especially when the application relies on path parameters without verifying ownership or scope.
Actix’s OpenAPI/Swagger integration can inadvertently compound this risk. If the spec documents endpoints with Basic Auth but does not accurately reflect runtime guard requirements, generated documentation may imply protections that are incomplete. During scans, middleBrick cross-references the spec definitions with runtime behavior; mismatches can highlight endpoints where authentication is declared but not enforced, or where sensitive fields are returned without proper authorization. For instance, a documented GET /users/{id} that should require scope-based access might respond with a full user object when a valid Basic Auth token is supplied, even if the token belongs to a low-privilege identity.
The framework itself does not introduce the disclosure; rather, the risk arises from how handlers, extractors, and guards are composed. An Actix application that uses middleware to enforce authentication but lacks consistent error handling can produce divergent responses. A 401 with a generic “Unauthorized” body is safer than one that distinguishes between “invalid password” and “user not found”. Similarly, logging raw headers or request paths without redaction may expose credentials or PII in operational outputs, turning routine debugging artifacts into an information leak channel.
To mitigate Information Disclosure in this context, ensure that all routes requiring credentials are uniformly protected, responses are consistent regardless of authentication state, and serialization excludes sensitive fields unless the requester is explicitly authorized. Leverage Actix’s extractor composition to centralize authorization checks and avoid scattering ad-hoc guards across handlers. Where OpenAPI specs are used, validate that runtime behavior aligns with declared security requirements, particularly for endpoints that involve identifiers or personal data. Tools like middleBrick can surface deviations between spec and runtime, highlighting routes where Basic Auth is declared but not correctly enforced or where response payloads risk exposing sensitive information.
Basic Auth-Specific Remediation in Actix — concrete code fixes
Remediation focuses on consistent authentication enforcement, safe error messages, and secure handling of credentials within Actix applications. Use dedicated extractors for Basic Auth, validate credentials against a secure store, and ensure all sensitive routes are guarded. Avoid leaking information via status codes or response bodies, and redact or omit sensitive data from serialization.
Below are concrete, syntactically correct examples for securing Actix endpoints with Basic Auth.
1. Secure Basic Auth extractor with validation
This example shows an Actix handler that uses a custom extractor to parse and validate credentials, returning a uniform 401 response on failure.
use actix_web::{web, HttpResponse, Error};
use actix_web::http::header;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::error::ErrorUnauthorized;
use futures::future::{ok, Ready};
use std::future::Future;
struct AuthenticatedUser {
pub user_id: String,
pub scopes: Vec<String>,
}
fn validate_credentials(username: &str, password: &str) -> Option<AuthenticatedUser> {
// Replace with secure lookup and constant-time verification
if username == "admin" && password == "correct_hard_to_guess_password" {
Some(AuthenticatedUser {
user_id: username.to_string(),
scopes: vec!["read:users".to_string(), "write:users".to_string()],
})
} else {
None
}
}
async fn basic_auth_extractor(
req: ServiceRequest,
credentials: (&str, &str),
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let (username, password) = credentials;
validate_credentials(username, password)
.map(|user| {
req.extensions_mut().insert(user);
req
})
.map_err(|_| (ErrorUnauthorized("Invalid credentials"), req))
}
async fn profile_handler(
user: web::ReqData<AuthenticatedUser>
) -> HttpResponse {
// Only safe, authorized data is returned
HttpResponse::Ok().json(serde_json::json!({
"user_id": user.user_id,
"scopes": user.scopes
}))
}
/// Apply to Actix App
/// App::new()
/// .wrap_fn(|req, srv| {
/// let (req, pl) = req.into_parts();
/// let auth_future = async move {
/// let creds = (req.headers().get(header::AUTHORIZATION)
/// .and_then(|v| v.to_str().ok())
/// .and_then(|s| s.strip_prefix("Basic "))
/// .and_then(|e| base64::decode(e).ok())
/// .and_then(|b| String::from_utf8(b).ok())
/// .and_then(|s| {
/// let parts: Vec<&str> = s.splitn(2, ':').collect();
/// if parts.len() == 2 { Some((parts[0], parts[1])) } else { None }
/// }));
/// match creds {
/// Some((user, pass)) => {
/// let user = validate_credentials(user, pass);
/// match user {
/// Some(u) => {
/// let mut req = actix_web::HttpRequest::from_parts(req, pl);
/// req.extensions_mut().insert(u);
/// let res = srv.call(req).await;
/// res
/// }
/// None => {
/// let res = HttpResponse::Unauthorized().finish();
/// Ok(actix_web::dev::ServiceResponse::new(req.into(), res))
/// }
/// }
/// }
/// None => {
/// let res = HttpResponse::Unauthorized().finish();
/// Ok(actix_web::dev::ServiceResponse::new(req.into(), res))
/// }
/// }
/// };
/// Box::pin(auth_future)
/// })
/// .service(web::resource("/profile").route(web::get().to(profile_handler)));
2. Consistent error handling and secure serialization
Ensure that authentication failures do not reveal whether a username exists, and avoid returning sensitive fields in error or success responses unless the caller is fully authorized.
use actix_web::{web, HttpResponse, Responder};
use serde::Serialize;
#[derive(Serialize)]
struct SafeProfile {
user_id: String,
// Do not serialize email or internal_role unless strictly necessary and authorized
}
async fn get_profile(user: web::ReqData<AuthenticatedUser>) -> impl Responder {
let safe = SafeProfile {
user_id: user.user_id.clone(),
};
HttpResponse::Ok().json(safe)
}
async fn auth_error() -> impl Responder {
// Generic message to avoid information leakage
HttpResponse::Unauthorized().body("Unauthorized")
}
3. Middleware guard to enforce authentication across selected routes
Use a guard that checks for valid credentials on protected routes and rejects unauthenticated requests uniformly.
use actix_web::{web, Error, HttpResponse};
use actix_web::dev::{Transform, ServiceRequest, ServiceResponse, ServiceError};
use actix_web::error::ErrorUnauthorized;
use std::task::{Context, Poll};
use futures::future::{ok, Ready};
pub struct AuthGuard;
impl Transform<S, ServiceRequest> for AuthGuard
where
S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthGuardMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthGuardMiddleware { service })
}
}
pub struct AuthGuardMiddleware<S> {
service: S,
}
impl<S, B> actix_web::dev::Service<ServiceRequest> for AuthGuardMiddleware<S>
where
S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let auth_header = req.headers().get(header::AUTHORIZATION);
let valid = match auth_header {
Some(h) => {
if let Ok(s) = h.to_str() {
if let Ok(decoded) = base64::decode(s.strip_prefix("Basic ").unwrap_or("")) {
if let Ok(creds) = String::from_utf8(decoded) {
let parts: Vec<&str> = creds.splitn(2, ':').collect();
parts.len() == 2 && validate_credentials(parts[0], parts[1]).is_some()
} else {
false
}
} else {
false
}
} else {
false
}
}
None => false,
};
if !valid {
let res = HttpResponse::Unauthorized().body("Unauthorized");
let res = ServiceResponse::new(req.into(), res);
return Box::pin(async { Ok(res) });
}
let fut = self.service.call(req);
Box::pin(async move { fut.await })
}
}
// In App configuration:
// .service(
// web::scope("/api")
// .wrap(AuthGuard)
// .service(web::resource("/profile").route(web::get().to(profile_handler)))
// )
4. Secure credential storage and constant-time checks
Store credentials using a salted, slow hash (e.g., Argon2id) and compare using a constant-time function to prevent timing attacks. Avoid plaintext comparisons in production code.
// Example using argon2 (add argon2 = "0.5" to Cargo.toml)
use argon2::password_hash::{SaltString, rand_core::OsRng};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
fn hash_password(plain: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2.hash_password(plain.as_bytes(), &salt).unwrap().to_string()
}
fn verify_password(plain: &str, stored_hash: &str) -> bool {
let parsed = PasswordHash::new(stored_hash).ok()?;
Argon2::default().verify_password(plain.as_bytes(), &parsed).is_ok()
}