Privilege Escalation in Axum
How Privilege Escalation Manifests in Axum
In Axum applications, privilege escalation typically emerges from two flawed assumptions: that authentication equals authorization, and that client-provided identifiers are trustworthy. Axum's extractor-based handler design, while ergonomic, can obscure authorization logic. A common vulnerable pattern is a handler that extracts a user ID from the request path or body and uses it to fetch or modify data without verifying that the authenticated user has rights to operate on that specific resource.
Horizontal Escalation (BOLA/IDOR): Consider an endpoint that allows users to update their profile. A vulnerable implementation might directly use the id path parameter to query the database, assuming the authenticated user can only access their own record. An attacker can simply change the path value (e.g., PUT /users/123 to PUT /users/456) to manipulate another user's data if no ownership check exists.
// VULNERABLE: No authorization check beyond authentication
#[derive(Deserialize)]
struct UpdateUserParams {
id: u64,
email: Option,
}
async fn update_user_handler(
Extension(user): Extension<User>, // User from auth middleware
Json(params): Json<UpdateUserParams>,
) -> impl IntoResponse {
let user = sqlx::query!(
"SELECT * FROM users WHERE id = ?",
params.id
).fetch_one(&pool).await.unwrap();
// ... update logic
} Vertical Escalation (BFLA): Axum apps often have role-based endpoints (e.g., /admin/users). A flaw occurs when role validation is missing or based on client-controlled data. For instance, an admin creation endpoint that trusts a role field in the request body allows any authenticated user to grant themselves administrative privileges.
// VULNERABLE: Role is taken from request body, not server-side session
#[derive(Deserialize)]
struct CreateUserRequest {
username: String,
password: String,
role: String, // Attacker sets this to "admin"
}
async fn create_user_handler(
Extension(user): Extension<User>,
Json(req): Json<CreateUserRequest>,
) -> impl IntoResponse {
// Only checking if user is authenticated, not if they have 'admin' role
sqlx::query!(
"INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)",
req.username,
hash(req.password),
req.role // Dangerous: client-controlled
).execute(&pool).await.unwrap();
}Axum's flexibility with extractors (e.g., Json<T>, Path<T>) can lead to implicit trust in deserialized data. Additionally, when using OpenAPI generators like utoipa, missing security or securitySchemes definitions in the spec can lead to runtime endpoints that lack documented authorization requirements, creating a gap between design and implementation.
Axum-Specific Detection
Detecting privilege escalation in Axum APIs requires testing for unauthorized access across different user contexts. Manual testing involves:
- Using tools like
curlor Postman to send requests with different authentication tokens (e.g., user vs. admin) to the same endpoint and observing if access rights change unexpectedly. - Manipulating path parameters (e.g.,
/users/{id}) or JSON body fields (e.g.,{"role":"admin"}) to probe for horizontal or vertical escalation.
Automated scanning with middleBrick systematically probes for Broken Function Level Authorization (BFLA) and Broken Object Level Authorization (BOLA). When you submit an Axum API endpoint, middleBrick:
- Identifies all actionable endpoints from the OpenAPI spec (if available) or by crawling, including those with path parameters like
/users/{id}. - Tests each endpoint with multiple privilege contexts: it first establishes a baseline authenticated session (often as a low-privilege user), then sends crafted requests that attempt to access higher-privilege functions or objects belonging to other users.
- Analyzes responses for status codes (e.g.,
200 OKinstead of403 Forbidden), data leakage, or state changes that indicate successful escalation.
For Axum APIs that publish an OpenAPI spec (e.g., via utoipa), middleBrick cross-references the spec's declared security requirements with runtime behavior. If the spec marks an endpoint as requiring admin scope but the runtime endpoint responds with 200 for a non-admin token, that's a clear finding.
You can scan your Axum API from the terminal using the middleBrick CLI:
middlebrick scan https://api.youraxumapp.comOr integrate scanning into your CI/CD pipeline for Axum projects using the middleBrick GitHub Action. This allows you to fail builds if a new privilege escalation vulnerability is introduced or if the security score drops below a threshold.
Axum-Specific Remediation
Remediation in Axum centers on enforcing server-side authorization checks that are independent of client input. The core principle: never trust identifiers or roles from the request; always derive authorization from the authenticated user's session and the target resource's ownership.
1. Implement Centralized Authorization Middleware
Create middleware that runs after authentication and attaches the user's roles/claims to the request Extension. This ensures every handler has access to the verified user identity.
use axum::{
async_trait, extract::{FromRequestParts, State},
http::{request::Parts, StatusCode},
response::Response,
};
use std::sync::Arc;
struct AuthUser {
id: u64,
roles: Vec<String>,
}
#[async_trait]
implement<FromRequestParts> for AuthUser {
async fn from_request_parts(
parts: &mut Parts,
state: &State<Arc<AppState>>,
) -> Result<Self, (StatusCode, String)> {
// Extract token, validate, fetch user from DB, attach roles
// This runs for every request after authentication
let user = verify_token(parts).await?;
Ok(user)
}
}
// Apply globally or per-route
let app = Router::new()
.route_layer(axum::middleware::from_fn(auth_middleware));2. Enforce Ownership Checks in Handlers
For resource-specific endpoints (e.g., /users/{id}), compare the path id against the authenticated user's ID (or their admin status).
async fn update_user_handler(
AuthUser { id: current_user_id, roles }: AuthUser, // From middleware
Path(user_id): Path<u64>,
Json(mut updates): Json<UpdateUserDto>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
// Horizontal check: users can only update themselves unless admin
if current_user_id != user_id && !roles.contains(&"admin".to_string()) {
return Err((StatusCode::FORBIDDEN, "Cannot update other users".into()));
}
// Proceed with update...
Ok(Json(update_user(user_id, updates).await?))
}3. Validate Role Claims for Privileged Endpoints
For admin-only routes, explicitly check for the required role. Do not rely on a role field from the client.
async fn admin_dashboard_handler(
AuthUser { roles, .. }: AuthUser,
) -> impl IntoResponse {
if !roles.contains(&"admin".to_string()) {
return (StatusCode::FORBIDDEN, "Admin access required");
}
// Admin logic...
}4. Keep OpenAPI Specs in Sync
If you generate an OpenAPI spec with utoipa, annotate endpoints with required security scopes. This documents intent and allows tools like middleBrick to verify compliance.
#[utoipa::path(
post,
path = "/admin/users",
security(("bearer_auth" = ["admin"])), // Declares admin scope required
responses(...)
)]
async fn create_admin_user_handler(...) -> ... { ... }5. Adopt Least Privilege
Design your data model so that users operate within a tenant or scope. Use database row-level security (e.g., PostgreSQL RLS) as a defense-in-depth layer, but never rely on it solely—application-level checks are essential.
After implementing fixes, rescan with middleBrick to verify that the BFLA/BOLA findings are resolved and that the security score improves. The Pro plan's continuous monitoring can alert you if a future code change reintroduces a privilege escalation flaw.