Credential Stuffing in Axum
Credential Stuffing in Axum
Credential stuffing attacks involve automated scripts that submit large volumes of username and password combinations against an API endpoint, hoping that some credentials will work. Axum, as a web framework built on Actix Web, receives these requests like any other HTTP call. Because Axum does not typically enforce rate limiting or account lockout mechanisms by default, an attacker can script thousands of POST requests to an authentication endpoint such as /api/v1/login, each containing a different username/password pair.
Common attack patterns include:
- Bulk submission of credential lists obtained from past data breaches, often formatted as JSON payloads like
{ "username": "admin", "password": "password123" }. - Use of bots that rotate IP addresses or use residential proxy services to avoid detection.
- Targeting endpoints that accept email addresses as identifiers, where the attacker may try multiple passwords per username.
These attacks manifest in Axum when the login endpoint does not differentiate between legitimate failed attempts and malicious enumeration. For example, an endpoint that returns a generic error message such as Invalid credentials for every failed login can be abused to programmatically test password validity. Since Axum applications often treat all POST requests to the authentication route as valid, there is no built-in protection against this volume of requests.
Real-world incidents have shown that APIs built with Axum have been scanned and found vulnerable to credential stuffing when:
- Authentication is handled via a simple
Authguard that checks credentials against a database without additional throttling. - Password reset or account lockout logic is either missing or disabled in production.
- Rate limiting is not enforced at the reverse proxy level, allowing unlimited request bursts.
Because Axum runs on the Rust runtime, the performance of handling many concurrent requests can be high, which attackers exploit to increase throughput of credential trials.
Detecting Credential Stuffing in Axum Using middleBrick
middleBrick can identify credential stuffing risks by analyzing the API's response behavior under simulated attack conditions. When scanning an Axum endpoint, middleBrick sends a series of POST requests with known weak credentials (e.g., admin:123456, admin:password, test:test) and observes the system's responses.
Key signals middleBrick looks for include:
- Repeated
200 OKresponses for multiple invalid credentials, indicating that the endpoint is accepting them without proper validation. - Consistent error messages such as
Invalid credentialsacross many requests, which suggests the application is not distinguishing between account existence and password correctness. - High volume of failed authentication attempts from a single source or distributed sources, which may trigger rate limiting rules if configured.
During a scan, middleBrick may also check for the presence of authentication endpoints that accept JSON payloads and lack multi-factor authentication (MFA) enforcement. If the endpoint returns a token or session identifier upon successful login, middleBrick will flag cases where weak credentials are accepted.
Example of a simulated request that middleBrick might send:
curl -X POST https://api.example.com/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'If the response contains a session token or cookie, middleBrick records this as a potential credential acceptance issue. The scanner then correlates this with other findings such as lack of rate limiting or missing account lockout mechanisms, contributing to a lower security score in the Authentication category of the report.
You can run this scan yourself using the CLI:
middlebrick scan https://api.example.com/auth/loginThe resulting report will include findings under the Authentication category with severity levels and remediation guidance tailored to Axum applications.
Remediating Credential Stuffing in Axum
To mitigate credential stuffing in an Axum application, you should implement controls that reduce the success rate of automated credential trials. The following code example demonstrates how to add rate limiting and account lockout logic using Axum's built-in extensions and third-party crates.
First, add the necessary dependencies to your Cargo.toml:
[dependencies]
axum = "0.7"
axum-extra = "0.7"
axum-extra-session = "0.7"
tokio = "1"
async-trait = "0.1"
throttler = "3.0"Next, configure a throttling layer that limits login attempts per IP address. This example uses the throttler crate to allow only 5 login attempts per minute per client:
use axum::{routing::post, Router, extract::Extension};
use throttler::{Tier, Throttler};
async fn login_handler() -> &'static str {
"Login successful"
}
#[tokio::main]
async fn main() {
// Create a throttler with a tier that allows 5 requests per minute per key
let throttler = Throttler::new()
.add_policy(Tier::One, 5)
.await;
let app = Router::new()
.route("/auth/login", post(login_handler))
.layer(Extension(throttler))
.layer(axum::middleware::from_fn(|req, _ext| {
// Extract the client IP address
if let Some(peer_addr) = req.connection().peer_addr() {
let ip = peer_addr.ip().to_string();
// You would store attempt counts in a store like Redis
// For demonstration, we just pass the IP forward
req.extensions_mut().insert(ip);
}
Ok::<(), axum::http::StatusCode>(axum::http::StatusCode::OK)
}));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await;
}In addition to rate limiting, you should implement account lockout after a configurable number of failed attempts. This can be done by storing failure counts in a database and rejecting further attempts until a cooldown period expires. Here is a simplified example using an in-memory store (replace with Redis or another persistent store in production):
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
struct AppState {
failure_counts: Arc>>,
}
async fn login_with_lockout(
Extension(state): Extension>>,
Json(payload): Json,
) -> Result {
let username = payload["username"].as_str().unwrap_or("").to_string();
let password = payload["password"].as_str().unwrap_or("").to_string();
if username != "admin" || password != "secret" {
let mut counts = state.read().await.failure_counts.write().await;
*counts.entry(username).or_insert(0) += 1;
if counts.get(&username).unwrap_or(&0) > 5 {
return Err((axum::http::StatusCode::LOCKED, "Account locked due to too many failed attempts".into()));
}
return Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid credentials".into()));
}
// Reset failure count on success
state.read().await.failure_counts.write().await.remove(&username);
Ok("Login successful")
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
failure_counts: Arc::new(RwLock::new(HashMap::new())),
});
let app = Router::new()
.route("/auth/login", post(login_with_lockout))
.layer(Extension(state))
.route("/health", axum::routing::get(|| async { "OK" }));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await;
} Additional best practices include:
- Enforcing HTTPS to prevent credential interception.
- Requiring CAPTCHA or bot detection challenges for high-risk endpoints.
- Implementing multi-factor authentication (MFA) for user accounts.
- Monitoring login patterns for anomalies using logging and alerting.
These measures, when combined with regular security scanning using middleBrick, significantly reduce the risk of credential stuffing attacks on Axum-based APIs.