HIGH time of check time of useaxumbasic auth

Time Of Check Time Of Use in Axum with Basic Auth

Time Of Check Time Of Use in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability

Time Of Check Time Of Use (TOCTOU) occurs when the outcome of a security check is not tightly coupled to the action that follows. In Axum, combining Basic Auth with a two-phase flow—first validating credentials, then using the identity in a business operation—creates a classic TOCTOU window. Between the check and the use, the authorization context can change, allowing an attacker to act under a different identity or elevated privileges.

Consider an endpoint that first verifies an Authorization header, resolves a user ID, and then performs a data operation based on path parameters (e.g., /users/{user_id}/documents/{document_id}). If the check confirms the user is alice with id 42, but immediately afterward the code reads document_id from the request and does not re-verify that document_id belongs to user 42, an attacker can change the runtime mapping (e.g., by altering a reverse proxy rule or a misconfigured load balancer) so that id 42 maps to a different principal by the time the document is accessed. Because Axum handlers are asynchronous and often composed of multiple layers, the gap between authentication and authorization is easy to introduce unintentionally.

Basic Auth exacerbates this because the credentials are sent on every request. If middleware caches the authentication result per connection or uses a lightweight, non-atomic check (e.g., checking the presence of a header without tying it to the downstream resource), the window widens. An attacker who can influence routing or session affinity might cause a subsequent request to be handled by a different service replica where the cached identity is stale or incorrectly mapped. This can lead to BOLA/IDOR when one user can access another user’s resources because the authorization check was performed once and reused across non-atomic steps.

In practice, TOCTOU with Basic Auth in Axum often surfaces in patterns where developers extract the username once and then rely on it across multiple function calls or database queries without re-binding the identity to each operation. For example, extracting the identity in a guard, storing it in request extensions, and then using that extension later to construct queries can be safe only if the extension is immutable and every downstream query re-applies ownership checks. If any layer assumes the identity cannot change between check and use, the system becomes vulnerable to tampering at the network or routing layer.

Real-world analogies include scenarios where an API uses Basic Auth for initial login but then relies on client-supplied IDs without verifying scope. An attacker could intercept or modify routing to make the same credentials map to different backend identities, or exploit a misconfigured proxy to forward requests on behalf of another user. Because Basic Auth transmits credentials each time, it is critical to ensure that every data access re-validates scope and ownership rather than assuming a prior check is sufficient.

Basic Auth-Specific Remediation in Axum — concrete code fixes

To prevent TOCTOU in Axum with Basic Auth, ensure that identity validation and resource ownership checks are performed atomically for each operation. Do not cache authorization decisions across multiple handler steps; instead, bind the identity to each data access and re-validate scope on every request.

Below are concrete, idiomatic Axum examples that demonstrate a safe pattern.

Example 1: Atomic handler with inline Basic Auth verification and ownership check

use axum::{
    async_trait,
    extract::Extension,
    http::header,
    response::IntoResponse,
    routing::get,
    Router,
};
use std::net::SocketAddr;
use tower_http::auth::{AuthLayer, Credentials, ValidateRequest};

struct UserId(pub i64);
struct DocumentId(pub i64);

struct AppState {
    // Shared data access, e.g., a repository or a pool
}

async fn get_document(
    Extension(state): Extension<AppState>,
    // Credentials are verified by AuthLayer and injected as Extensions
    Extension(identity): Extension<UserId>,
    // Path parameters are extracted per request
    axum::extract::Path((document_id,)):(DocumentId,),
) -> impl IntoResponse {
    // Atomic check: re-validate that the document belongs to the identity
    // This should map to a database query like:
    // SELECT 1 FROM documents WHERE id = $1 AND owner_id = $2
    let owned = verify_document_ownership(identity.0, document_id.0).await;
    if !owned {
        return ("Forbidden",).into_response();
    }
    // Safe to proceed
    format!("Document {} for user {}", document_id.0, identity.0)
}

async fn verify_document_ownership(user_id: i64, document_id: i64) -> bool {
    // Placeholder: replace with actual DB/query logic
    // Example: sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM documents WHERE id = $1 AND owner_id = $2)")
    //   .bind(document_id)
    //   .bind(user_id)
    //   .fetch_one(pool)
    //   .await
    user_id > 0 && document_id > 0
}

#[tokio::main]
async fn main() {
    let state = AppState {};

    // AuthLayer validates Basic Auth credentials on each request and provides UserId
    let validate = |credentials: Credentials| async move {
        // Validate username/password against your user store
        // In a real app, use constant-time checks and proper password hashing
        if credentials.username == "alice" && credentials.password == "secret" {
            Ok(UserId(42))
        } else {
            Err(()) // rejection
        }
    };

    let auth_layer = AuthLayer::new(validate);

    let app = Router::new()
        .route(
            "/users/:user_id/documents/:document_id",
            get(get_document),
        )
        .layer(auth_layer)
        .layer(Extension(state));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Example 2: Using tower-http AuthLayer with Basic Auth and per-route validation

use axum::{
    extract::Path,
    http::StatusCode,
    response::IntoResponse,
    Extension,
    Router,
};
use serde::Deserialize;
use std::sync::Arc;
use tower_http::auth::{AuthLayer, Credentials, Reject};

#[derive(Clone)]
struct UserRepository(Arc<()>); // replace with actual DB/pool

async fn validate_basic_auth(
    credentials: Credentials,
    repo: Extension<UserRepository>,
) -> Result<UserId, Reject> {
    // Perform lookup and constant-time password verification here
    // For demonstration, accept only a specific user
    if credentials.username == "alice" && credentials.password == "secret" {
        Ok(UserId(42))
    } else {
        Err(Reject)
    }
}

async fn fetch_document(
    Extension(repo): Extension<UserRepository>,
    Extension(user): Extension<UserId>,
    Path((_user_id, document_id)): Path<(i64, i64)>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    // Re-validate ownership on every request
    let owns = check_document_ownership(&repo, user.0, document_id).await;
    if !owns {
        return Err((StatusCode::FORBIDDEN, "Not allowed".to_string()));
    }
    Ok(format!("Document data for {}", document_id))
}

async fn check_document_ownership(repo: &UserRepository, user_id: i64, document_id: i64) -> bool {
    // Implement actual query; return true if document belongs to user
    user_id > 0 && document_id > 0
}

#[tokio::main]
async fn main() {
    let repo = UserRepository(Arc::new(()));

    let app = Router::new()
        .route(
            "/users/:user_id/documents/:document_id",
            get(fetch_document),
        )
        .layer(AuthLayer::new(validate_basic_auth).with_rejection_type<Reject>())
        .layer(Extension(repo));

    let addr = ("127.0.0.1", 3000).into();
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Key principles applied:

  • Authenticate on every handler entry using AuthLayer; do not rely on cached results across multiple asynchronous steps.
  • Re-check ownership in the same handler that accesses the resource, using the identity bound to the request.
  • Treat path parameters (e.g., user_id, document_id) as untrusted input and verify they map to the authenticated identity immediately before use.
  • Avoid global or request-extension caches for authorization decisions unless they are immutable and re-validated per operation.

Frequently Asked Questions

Why is Basic Auth more prone to TOCTOU in Axum if the credentials are verified each request?
Basic Auth sends credentials with every request, which can encourage developers to authenticate once and reuse the identity across multiple steps. If authorization checks are separated from data access or cached without re-binding to each operation, an attacker can exploit timing or routing changes between check and use. The fix is to authenticate and re-validate ownership atomically within the handler.
Does using middleware that caches authentication eliminate TOCTOU?
Caching authentication without re-checking ownership before each data operation introduces a TOCTOU window. In Axum, store authentication details in request extensions only as immutable metadata, and ensure every handler re-applies scope and ownership checks against the current request context.