Insecure Direct Object Reference in Axum
How Insecure Direct Object Reference Manifests in Axum
Insecure Direct Object Reference (IDOR) in Axum applications typically occurs when route parameters or query parameters are used directly to access data without proper authorization checks. Axum's macro-based routing system makes it easy to create endpoints that accept identifiers, but developers often forget to validate whether the authenticated user has permission to access the requested resource.
The most common pattern in Axum is using path parameters like /users/{user_id} or /orders/{order_id} and directly querying the database with these values. Since Axum extracts these parameters as strongly typed values (using the FromRequestParts trait), developers might assume the framework handles security, but it only handles parsing and validation of the parameter format, not authorization.
Consider this vulnerable Axum route:
async fn get_user_profile(user_id: String, db: DbPool) -> impl IntoResponse {
let user = sqlx::query_as!(User,
"SELECT * FROM users WHERE id = $1",
user_id
)
.fetch_one(&db)
.await?;
Ok(Json(user))
}
let app = Router::new()
.route("/users/:user_id", get(get_user_profile));
This code is vulnerable because it trusts the user_id parameter without verifying that the authenticated user has permission to view this profile. An attacker can simply change the user_id in the URL to access any user's data.
Another Axum-specific manifestation occurs with query parameters for pagination or filtering. Developers often use ?page=2&per_page=10 parameters directly in database queries without validating that the requesting user has access to all the records being returned. This can lead to data exposure across user boundaries when combined with improper database queries.
Collection-based endpoints are also vulnerable. An endpoint like /api/v1/users/{user_id}/orders might return all orders for a user ID without checking if the authenticated user owns those orders or has admin privileges to view them.
Axum-Specific Detection
Detecting IDOR vulnerabilities in Axum applications requires both manual code review and automated scanning. The macro-based routing system in Axum creates clear patterns that can be analyzed for security issues.
Manual detection should focus on routes that accept path parameters and query parameters. Look for patterns where database queries use these parameters directly without authorization checks. In Axum, this often appears in async fn handlers that take parameters extracted from the request path or query string.
middleBrick's black-box scanning approach is particularly effective for Axum applications because it tests the actual running API without requiring source code access. The scanner sends requests to your Axum endpoints with modified identifiers to check if authorization is properly enforced. For example, it might:
- Authenticate as one user, then request resources using another user's ID
- Modify numeric IDs in URLs to access different records
- Test collection endpoints with various user contexts
- Check if error messages reveal information about resource existence
The scanner's 12 security checks include specific tests for BOLA (Broken Object Level Authorization), which directly targets IDOR vulnerabilities. It runs these tests in parallel across your Axum API endpoints, providing a security score and detailed findings within 5-15 seconds.
For OpenAPI/Swagger analysis, middleBrick can examine your Axum application's generated spec to identify endpoints that accept identifiers, then correlate this with runtime scanning results. This is especially useful for Axum applications that use the axum::extract::OpenApi or similar tools to generate API documentation.
Key indicators to look for during detection:
- Routes with path parameters like
{id},{user_id},{order_id} - Query parameters used in database queries without validation
- Missing authorization middleware in handler chains
- Database queries that don't filter by user context
Axum-Specific Remediation
Remediating IDOR vulnerabilities in Axum requires implementing proper authorization checks before data access. Axum's modular design makes it straightforward to add authorization layers to your handlers.
The most effective approach is to use Axum's middleware system or extractors to enforce authorization. Here's a secure pattern using a custom extractor:
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
type DbPool = sqlx::Pool;
#[derive(Deserialize)]
struct UserIdParam(String);
async fn get_user_profile(
Path(UserIdParam(user_id)): Path<UserIdParam>,
auth_user: AuthenticatedUser,
db: DbPool,
) -> Result<impl IntoResponse, StatusCode> {
// Check if authenticated user can access this profile
if auth_user.id != user_id && !auth_user.is_admin {
return Err(StatusCode::FORBIDDEN);
}
let user = sqlx::query_as!(User,
"SELECT * FROM users WHERE id = $1",
user_id
)
.fetch_one(&db)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
// Usage in router
let app = Router::new()
.route("/users/:user_id", get(get_user_profile));
This pattern ensures that users can only access their own profile unless they have admin privileges. The AuthenticatedUser extractor would be implemented to extract user information from a JWT token or session.
For collection endpoints, implement row-level security by filtering database queries:
async fn get_user_orders(
Path(user_id): Path<String>,
auth_user: AuthenticatedUser,
db: DbPool,
) -> Result<impl IntoResponse, StatusCode> {
if auth_user.id != user_id && !auth_user.is_admin {
return Err(StatusCode::FORBIDDEN);
}
let orders = sqlx::query_as!(Order,
"SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC",
user_id
)
.fetch_all(&db)
.await?;
Ok(Json(orders))
}
For more complex authorization scenarios, consider using a dedicated authorization library like async-graphql or implementing a permissions system. You can also create reusable middleware for common authorization patterns:
async fn require_ownership_or_admin(
user_id: &str,
auth_user: &AuthenticatedUser,
) -> Result<(), StatusCode> {
if auth_user.id == user_id || auth_user.is_admin {
Ok(())
} else {
Err(StatusCode::FORBIDDEN)
}
}
Remember to handle errors consistently and avoid information leakage. Don't reveal whether a resource exists or not through different error responses. Use generic error messages for both unauthorized and non-existent resources.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |