Integrity Failures in Axum
How Integrity Failures Manifests in Axum
Integrity failures in Axum applications occur when unauthorized users can manipulate data or bypass authorization checks, leading to data corruption, unauthorized modifications, or privilege escalation. These vulnerabilities stem from Axum's asynchronous architecture and its reliance on extractors for request handling.
The most common manifestation is Missing Authorization Checks. Axum developers often extract user data from JWT tokens or session cookies, then forget to verify whether the authenticated user has permission to perform the requested operation. For example:
async fn update_profile(mut req: Json<UpdateProfile>) -> impl Responder {
let update = req.0;
// Missing: verify user owns this profile
user_repo.update(update).await?;
Ok("Profile updated")
}This endpoint allows any authenticated user to update any profile, creating a classic BOLA (Broken Object Level Authorization) vulnerability.
Property Authorization Bypass is another critical failure pattern. Axum's flexible data extraction can lead to scenarios where sensitive fields are exposed without proper authorization. Consider an endpoint that returns user objects with admin flags:
async fn get_user(&Path<UserId> id) -> impl Responder {
let user = user_repo.find(id).await?;
// Missing: check if requester is admin
Ok(Json(user))
}Any authenticated user can now discover which accounts have administrative privileges, enabling targeted attacks.
Race Condition Integrity Failures are particularly insidious in Axum due to its async/await model. When multiple requests modify the same resource concurrently, integrity checks performed before the modification can become stale:
async fn transfer_funds(
&Path<(UserId, UserId)> (from, to),
Json<Transfer> amount
) -> impl Responder {
// Check 1: Verify sufficient balance
let balance = account_repo.get_balance(from).await?;
if balance < amount { return Err(InsufficientFunds); }
// Check 2: Verify account active
let active = account_repo.is_active(from).await?;
if !active { return Err(AccountInactive); }
// Race condition window: checks complete, but state may have changed
account_repo.transfer(from, to, amount).await?;
Ok("Transfer complete")
}Between the authorization checks and the actual transfer, another request could modify the account state, violating integrity.
Unsafe Consumption of External Data manifests when Axum endpoints trust data from external APIs without validation. This can lead to integrity violations when malicious data corrupts application state:
async fn sync_external_data(
Json<ExternalPayload> payload
) -> impl Responder {
// No validation of external source authenticity
data_repo.merge(payload.data).await?;
Ok("Data synced")
}Without verifying the source and validating the payload, an attacker could inject malformed data that breaks application invariants.
Axum-Specific Detection
Detecting integrity failures in Axum requires understanding its unique patterns and architectural decisions. The most effective approach combines static analysis with runtime scanning.
Code Pattern Analysis reveals common anti-patterns. Look for these red flags in your Axum codebase:
// Anti-pattern: Missing authorization in async handlers
async fn delete_resource(
&Path<ResourceId> id,
auth: AuthToken // Extracted but not verified
) -> impl Responder {
resource_repo.delete(id).await?; // No ownership check
Ok("Deleted")
}Tools like cargo-audit and custom Rust analyzers can flag these patterns automatically.
middleBrick's Runtime Scanning specifically targets Axum applications through black-box testing. The scanner identifies integrity failures by:
- Testing authenticated endpoints with different user contexts to detect missing authorization
- Analyzing OpenAPI specs to understand expected authorization patterns
- Probing for property exposure vulnerabilities
- Testing concurrent request scenarios to uncover race conditions
For Axum applications, middleBrick provides particularly valuable insights because it tests the actual running application rather than just static code analysis. The scanner can detect subtle issues like:
// Vulnerability middleBrick would detect:
async fn view_private_data(
&Path<UserId> id,
auth: AuthenticatedUser // Extracted but ownership not verified
) -> impl Responder {
let data = private_data_repo.get(id).await?;
Ok(Json(data)) // Any authenticated user can access any data
}Property Authorization Testing is crucial for Axum apps. middleBrick analyzes your API's response structure and tests whether sensitive properties are properly protected. For example, if your API returns user objects with is_admin fields, middleBrick will attempt to access these properties with different user roles to verify proper authorization.
Rate Limiting Verification helps detect integrity failures that could occur through abuse. Axum applications without proper rate limiting are vulnerable to brute force attacks that can eventually bypass integrity controls through sheer volume of attempts.
Axum-Specific Remediation
Remediating integrity failures in Axum requires leveraging Rust's type system and Axum's middleware architecture. Here are specific patterns and solutions:
Authorization Middleware provides a reusable layer for integrity checks:
use axum::{extract::Extension, http::StatusCode};
struct AuthenticatedUser { id: UserId, role: Role }
async fn require_ownership(
auth: AuthenticatedUser,
resource_id: ResourceId,
Extension(repo): Extension<ResourceRepo>
) -> Result<(), (StatusCode, String)> {
let resource = repo.find(resource_id).await?;
if resource.owner_id != auth.id {
return Err((StatusCode::FORBIDDEN, "Not authorized".to_string()));
}
Ok(())
}
// Apply to routes
async fn update_resource(
auth: AuthenticatedUser,
&Path<ResourceId> id,
Json<Update> update,
Extension(repo): Extension<ResourceRepo>
) -> Result<Json<Resource>, StatusCode> {
// Verify ownership before proceeding
require_ownership(auth, id, Extension(repo.clone())).await?;
let updated = repo.update(id, update, auth.id).await?;
Ok(Json(updated))
}Property-Based Authorization uses Rust's type system to enforce field-level access control:
use axum::response::IntoResponse;
#[derive(serde::Serialize)]
struct UserResponse<'a> {
id: &'a UserId,
name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<&'a str>,
// Admin field only included for admins
#[serde(skip_serializing_if = "Option::is_none")]
is_admin: Option<bool>,
}
async fn get_user(
&Path<UserId> id,
auth: AuthenticatedUser,
Extension(repo): Extension<UserRepo>
) -> impl IntoResponse {
let user = repo.find(id).await?;
let response = if auth.role == Role::Admin {
UserResponse {
id: &user.id,
name: &user.name,
email: Some(&user.email),
is_admin: Some(user.is_admin),
}
} else {
UserResponse {
id: &user.id,
name: &user.name,
email: None,
is_admin: None,
}
};
Ok(Json(response))
}Optimistic Concurrency Control prevents race condition integrity failures:
use axum::extract::Extension;
use tokio::sync::RwLock;
struct Account { id: UserId, balance: i64, version: u64 }
async fn transfer_funds_atomic(
&Path<(UserId, UserId)> (from, to),
Json<Transfer> amount,
Extension(repo): Extension<RwLock<AccountRepo>>
) -> Result<Json<Transaction>, StatusCode> {
let repo = repo.read().await;
// Perform all checks in a single transaction
let from_account = repo.find(from).await?;
let to_account = repo.find(to).await?;
if from_account.balance < amount.0 {
return Err(StatusCode::BAD_REQUEST);
}
// Perform transfer atomically
let mut repo = repo.write().await;
let from_account = repo.find(from).await?;
let to_account = repo.find(to).await?;
// Verify state hasn't changed
if from_account.balance != from_account.balance {
return Err(StatusCode::CONFLICT);
}
repo.update_balance(from, from_account.balance - amount.0).await?;
repo.update_balance(to, to_account.balance + amount.0).await?;
Ok(Json(Transaction::new(from, to, amount.0)))
}Input Validation Middleware ensures external data doesn't compromise integrity:
use axum::middleware::Next;
async fn validate_external_input(
req: axum::http::Request,
next: Next,
validator: ExternalDataValidator
) -> impl IntoResponse {
// Validate payload structure and constraints
if let Some(json) = req.extensions().get::<Json<ExternalPayload>>() {
if !validator.is_valid(json) {
return (StatusCode::BAD_REQUEST, "Invalid external data");
}
}
next.run(req).await
}
// Apply as middleware to specific routes
let app = Router::new()
.route("/api/sync", post(sync_external_data))
.layer(validate_external_input_layer());