Auth Bypass in Axum
How Auth Bypass Manifests in Axum
Auth bypass in Axum applications typically occurs when authentication middleware is improperly configured or when route handlers bypass security checks. Since Axum is an async web framework built on Tokio, these vulnerabilities often stem from incorrect middleware ordering, missing guards, or improper extraction of authentication state.
One common pattern is middleware that doesn't properly propagate authentication state. Consider an extractors-based approach where you extract a user from a database:
async fn user_from_token(
Authorization(
Bearer { token },
): Authorization,
) -> Result<UserId, Rejection> {
// Validate token and fetch user
let user = validate_token(token).await?;
Ok(user.id)
}
async fn get_profile(
user_id: UserId,
user: UserId,
) -> impl IntoResponse {
// This should fail if user_id != user
if user_id != user {
return (StatusCode::FORBIDDEN, "Access denied");
}
// Return profile
}
The vulnerability here is that user_from_token is called independently for each argument. If the token extraction fails silently or returns a default value, an attacker could manipulate the user_id parameter to access another user's data.
Another Axum-specific auth bypass occurs with Either and conditional middleware. When using Either::A or Either::B to handle different authentication states, a missing else branch can leave routes unprotected:
let api = Router::new()
.route("/admin", get(admin_handler))
.layer(Authenticate::bearer())
.or(Router::new()
.route("/public", get(public_handler))
.layer(Authenticate::optional())
);
If the optional() middleware doesn't properly handle unauthenticated requests, protected routes might be accessible without credentials.
State extraction vulnerabilities are particularly dangerous in Axum. When using State<T> extractors with shared mutable state, race conditions can lead to auth bypass:
async fn update_profile(
user_id: UserId,
data: Json<ProfileUpdate>,
state: State<Arc<Mutex<UserStore>>>,
) -> Result<impl IntoResponse, Error> {
let mut store = state.lock().await;
// No check that user_id matches the authenticated user
store.update_profile(user_id, data.0).await?;
Ok(StatusCode::OK)
}
Here, the function trusts the user_id parameter without verifying it matches the authenticated user, allowing an attacker to modify any profile by crafting requests with different user IDs.
Axum-Specific Detection
Detecting auth bypass in Axum applications requires understanding both the framework's middleware system and common Rust patterns. The key is to examine how authentication state flows through your application.
First, audit your middleware stack. In Axum, middleware applies to entire route sets, so check for missing guards on sensitive routes:
let app = Router::new()
.route("/api/*", get(api_handler))
.layer(Authenticate::bearer());
// This route bypasses authentication!
let public = Router::new()
.route("/admin/*", get(admin_handler));
The /admin routes are completely unprotected because they're on a separate router that never received the authentication layer.
Extractors provide another detection vector. Look for extractors that can fail silently or return default values:
async fn extract_user(
Authorization(
Bearer { token },
): Authorization,
) -> Result<UserId, Rejection> {
// If token is invalid, return a default user ID
if let Ok(user) = validate_token(token).await {
Ok(user.id)
} else {
Ok(UserId(0)) // Vulnerable: default user bypasses auth
}
}
This pattern allows unauthenticated requests to proceed with a default user ID, potentially accessing public data or triggering unintended behavior.
middleBrick's black-box scanning approach is particularly effective for Axum applications because it tests the actual HTTP endpoints without needing source code. The scanner sends authenticated and unauthenticated requests to each endpoint, checking for:
- Access control bypass by manipulating request parameters
- Missing authentication on sensitive endpoints
- Authentication state that doesn't persist across requests
- Default values that allow unauthenticated access
For example, middleBrick would test if an endpoint like GET /api/user/123 returns different data when authenticated as user 123 versus unauthenticated or authenticated as user 456.
Axum-Specific Remediation
Securing Axum applications against auth bypass requires proper middleware ordering, robust extractors, and careful state management. Here's how to implement these fixes using Axum's native features.
First, ensure proper middleware ordering. Authentication should wrap all protected routes:
let api = Router::new()
.route("/api/*", get(api_handler))
.layer(Authenticate::bearer())
.or(Router::new()
.route("/public/*", get(public_handler))
);
For more granular control, use extractor_middleware to protect individual routes:
use axum_extra::extractors::extractor_middleware;
async fn auth_middleware(
req: Request,
next: Next,
) -> Result<impl IntoResponse, Rejection> {
let auth_header = req.headers().get("authorization");
if let Some(header) = auth_header {
// Validate token
if validate_token(header).await? {
return next.run(req).await;
}
}
Err(Rejection::new(StatusCode::UNAUTHORIZED))
}
let app = Router::new()
.route("/sensitive/*", get(sensitive_handler))
.layer(extractor_middleware(auth_middleware));
Extractors should never return default values that bypass authentication. Instead, use proper error handling:
async fn extract_authenticated_user(
Authorization(
Bearer { token },
): Authorization,
) -> Result<UserId, Rejection> {
match validate_token(token).await {
Ok(user) => Ok(user.id),
Err(_) => Err(Rejection::new(StatusCode::UNAUTHORIZED)),
}
}
async fn get_user_profile(
user_id: UserId,
authenticated_user: UserId,
) -> impl IntoResponse {
// Verify the requested user matches the authenticated user
if user_id != authenticated_user {
return (StatusCode::FORBIDDEN, "Access denied");
}
// Return profile
}
For state-based operations, use Arc<Mutex<>> carefully and always verify permissions before accessing shared state:
async fn update_user_data(
user_id: UserId,
authenticated_user: UserId,
data: Json<UserData>,
state: State<Arc<Mutex<UserStore>>>,
) -> Result<impl IntoResponse, Error> {
if user_id != authenticated_user {
return Err(Error::from(StatusCode::FORBIDDEN));
}
let mut store = state.lock().await;
store.update_data(user_id, data.0).await?;
Ok(StatusCode::OK)
}
Finally, use Axum's TypedHeader extractor for more robust header parsing and validation:
use axum::extract::TypedHeader;
use axum::headers::authorization::Bearer;
use axum::headers::authorization::Authorization;
async fn protected_endpoint(
TypedHeader(Authorization(Bearer { token })): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Rejection> {
// Token is guaranteed to be present and properly formatted
let user = validate_token(token).await?;
Ok(Json(user))
}
This approach ensures that authentication headers are properly parsed and validated before reaching your business logic.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |