Time Of Check Time Of Use in Axum
How Time Of Check Time Of Use Manifests in Axum
Time Of Check Time Of Use (TOCTOU) is a critical race condition vulnerability that affects Axum applications when the state of a resource changes between the check and the use phases. In Axum, this typically manifests in concurrent request handling scenarios where shared mutable state is accessed without proper synchronization.
Consider an Axum endpoint that checks if a user has sufficient balance before processing a transaction. The classic TOCTOU pattern emerges when the balance check and the actual deduction happen as separate operations:
async fn transfer_funds(
state: web::Data<AppState>,
user_id: web::Path<String>,
req: web::Json<TransferRequest>,
) -> Result<impl IntoResponse> {
// TOCTOU vulnerability: check and use are separate operations
let current_balance = state.accounts.get(&user_id).await;
if current_balance.unwrap_or(0) < req.amount {
return Ok(ApiError::InsufficientFunds);
}
// Another request could modify the balance here
state.accounts.update_balance(&user_id, -req.amount).await?;
Ok(())
}The race condition occurs because the balance check and the update are separate operations. Between these operations, another concurrent request could modify the same account, leading to overdrafts or other inconsistencies.
Another Axum-specific TOCTOU scenario involves async lock contention. When using async Mutex or RwLock from tokio for shared state management, the unlock operation between check and use creates a window for race conditions:
async fn update_profile(
state: web::Data<AppState>,
user_id: web::Path<String>,
req: web::Json<ProfileUpdate>,
) -> Result<impl IntoResponse> {
// TOCTOU window: lock is released between check and use
let mut lock = state.user_profiles.lock().await;
let profile = lock.get(&user_id).cloned();
if profile.is_none() {
return Ok(ApiError::ProfileNotFound);
}
// Lock is dropped here, allowing other operations
drop(lock);
// State could have changed by now
let mut lock = state.user_profiles.lock().await;
lock.insert(user_id.clone(), req.into_inner());
Ok(())
}TOCTOU also appears in Axum's extractors when dealing with request body parsing and validation. The separation between extraction and processing creates opportunities for malicious actors to manipulate the request state:
async fn process_order(
state: web::Data<AppState>,
order: web::Json<Order>,
) -> Result<impl IntoResponse> {
// TOCTOU: validation happens during extraction, processing later
let order = order.into_inner();
// Another request could modify inventory between validation and processing
if !state.inventory.has_sufficient_stock(&order.items).await {
return Ok(ApiError::OutOfStock);
}
// Process order - inventory could have changed
state.orders.create(order).await?;
Ok(())
}The core issue in all these patterns is the temporal gap between when a condition is verified and when the corresponding action is taken, creating a window where concurrent operations can invalidate the initial check.
Axum-Specific Detection
Detecting TOCTOU vulnerabilities in Axum requires understanding both the async nature of Rust and Axum's specific patterns. The first step is identifying shared mutable state that's accessed across async boundaries without proper synchronization.
middleBrick's security scanning engine specifically targets TOCTOU patterns in Axum applications by analyzing the control flow between state checks and state modifications. The scanner identifies three critical indicators:
- Async operations between check and use phases
- Shared mutable state accessed without atomic operations
- Multiple lock acquisitions on the same resource
For Axum applications, middleBrick examines the AST to find patterns like:
// Pattern middleBrick flags as TOCTOU risk
let value = state.shared.get(key).await;
if value.is_valid() {
// Async operation here creates race window
state.shared.update(key, new_value).await?;
}The scanner also detects TOCTOU in Axum's extractor system by analyzing the separation between request extraction and business logic execution. It flags patterns where:
- Request body is extracted and validated separately from processing
- Database queries are performed before async operations that could modify the same data
- Multiple async operations access the same resource without transactional boundaries
middleBrick's LLM/AI security module adds another layer of detection for TOCTOU in AI-powered Axum endpoints. It scans for patterns where:
async fn generate_response(
state: web::Data<AppState>,
prompt: web::Json<Prompt>,
) -> Result<impl IntoResponse> {
// TOCTOU in AI context: system prompt checked, then modified
let system_prompt = state.llm.get_system_prompt().await;
if system_prompt.contains("forbidden") {
return Ok(ApiError::Unauthorized);
}
// Another request could modify the system prompt here
let response = state.llm.generate(prompt.content).await;
Ok(response)
}For manual detection in Axum codebases, developers should look for:
- Async operations between state validation and state mutation
- Multiple lock acquisitions on the same resource
- Database reads followed by writes without transactions
- Shared state accessed across async boundaries
middleBrick's dashboard provides TOCTOU-specific risk scores with severity levels based on the potential impact and exploitability of the race condition, helping teams prioritize remediation efforts.
Axum-Specific Remediation
Remediating TOCTOU vulnerabilities in Axum requires atomic operations that combine check and use phases into a single, indivisible operation. The most effective approach is using database transactions or in-memory atomic operations.
For database-backed state, use Axum with sqlx's transaction support:
async fn transfer_funds_atomic(
db: PgPool,
req: web::Json<TransferRequest>,
) -> Result<impl IntoResponse> {
// Single atomic transaction eliminates TOCTOU
let result = sqlx::transaction::Transaction::new(db.clone(), None).await?;
let from_balance: i64 = sqlx::query_as(
"SELECT balance FROM accounts WHERE user_id = $1 FOR UPDATE"
)
.bind(&req.from_user)
.fetch_one(&result)
.await?
.get(0);
if from_balance < req.amount {
return Ok(ApiError::InsufficientFunds);
}
// Both operations in same transaction
sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE user_id = $2")
.bind(req.amount)
.bind(&req.from_user)
.execute(&result)
.await?;
sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE user_id = $2")
.bind(req.amount)
.bind(&req.to_user)
.execute(&result)
.await?;
result.commit().await?;
Ok(())
}For in-memory state, use tokio's Mutex or RwLock with careful design to avoid TOCTOU windows:
async fn update_profile_atomic(
state: web::Data<AppState>,
user_id: web::Path<String>,
req: web::Json<ProfileUpdate>,
) -> Result<impl IntoResponse> {
// Single lock acquisition eliminates TOCTOU
let mut profiles = state.user_profiles.lock().await;
// Check and update within same lock
if let Some(existing) = profiles.get_mut(&user_id) {
*existing = req.into_inner();
} else {
return Ok(ApiError::ProfileNotFound);
}
Ok(())
}For Axum applications using async-std or tokio, leverage compare-and-swap operations:
use tokio::sync::RwLock;
async fn process_order_atomic(
state: web::Data<AppState>,
order: web::Json<Order>,
) -> Result<impl IntoResponse> {
// Use atomic compare-and-swap pattern
let result = state.inventory.compare_and_swap(
&order.items,
|current_items| {
// Check if sufficient stock
current_items.iter().all(|(item, qty)| {
state.stock.get(item).unwrap_or(0) >= qty
})
},
|current_items| {
// Deduct stock if check passes
for (item, qty) in current_items {
let current = state.stock.get_mut(item).unwrap();
*current -= qty;
}
},
).await;
if !result {
return Ok(ApiError::OutOfStock);
}
// Process order after atomic inventory update
state.orders.create(order.into_inner()).await?;
Ok(())
}For Axum applications with Redis backend, use Redis transactions or Lua scripts for atomic operations:
async fn transfer_funds_redis(
redis: &mut redis::aio::Connection,
req: &TransferRequest,
) -> Result<impl IntoResponse> {
// Redis transaction eliminates TOCTOU
let mut transaction = redis.multi().await?;
// Check balance and prepare updates
transaction.decr("balance:".to_string() + &req.from_user, req.amount as isize).await?;
transaction.incr("balance:".to_string() + &req.to_user, req.amount as isize).await?;
// Execute atomically
let result = transaction.exec().await?;
// Check if from_user had sufficient balance
if result.is_err() {
// Rollback by reverting the to_user increment
redis.decr("balance:".to_string() + &req.to_user, req.amount as isize).await?;
return Ok(ApiError::InsufficientFunds);
}
Ok(())
}The key principle across all these patterns is ensuring that check and use operations are performed within the same atomic context, eliminating the race condition window entirely.