Insecure Direct Object Reference in Axum with Mutual Tls
Insecure Direct Object Reference in Axum with Mutual Tls
Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes internal object references (e.g., database IDs, filenames) without verifying that the requesting user has permission to access that specific resource. In Axum, a common pattern is to extract a user identifier from a request path or query parameter and use it directly to fetch data, such as user_id from the URL to load a profile. Even when Mutual TLS (mTLS) is enforced at the transport layer, mTLS authenticates the client to the server using certificates, but it does not inherently enforce authorization between one authenticated client and another resource belonging to a different client. If the server uses the mTLS client certificate to identify the client (e.g., via a subject or SAN field) but then fails to confirm that the requested resource (e.g., /users/{user_id}) belongs to that authenticated client, an IDOR vulnerability remains.
Consider an Axum handler that retrieves a user profile by ID from the URL while relying on mTLS for authentication:
use axum::{routing::get, Router, extract::State};
use std::net::SocketAddr;
struct AppState { /* ... */ }
async fn get_user_profile(
State(state): State,
user_id: String, // extracted from path, e.g., /users/{user_id}
) -> String {
// Vulnerable: no check that the authenticated client owns user_id
format!("Profile for {}", user_id)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:user_id", get(get_user_profile))
.with_state(AppState { /* ... */ });
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
In this setup, mTLS might ensure that each request presents a valid client certificate, but if the server maps the certificate to a user identity and then directly uses the user-supplied user_id without verifying that the authenticated user matches user_id, an attacker can change the path to access another user’s data. This is a classic BOLA/IDOR: authentication (mTLS) is present, but authorization (resource ownership check) is missing. The risk is compounded when internal identifiers are predictable (sequential integers or UUIDs), enabling enumeration of other users’ data.
Even with mTLS, server-side logic must treat the authenticated principal as an assertion of identity, not as proof of entitlement to a specific resource. Without explicit checks tying the resource to the authenticated principal, the API remains vulnerable regardless of the strength of the TLS mutual authentication.
Mutual Tls-Specific Remediation in Axum
Remediation focuses on ensuring that after mTLS authentication establishes the client’s identity, the server enforces that the authenticated principal can only access resources they own or are permitted to access. In Axum, you typically extract the authenticated principal from the request extensions (populated by a middleware that reads the client certificate) and compare it with the resource owner before proceeding.
First, implement middleware that extracts the authenticated identity from the mTLS certificate and attaches it to the request extensions. Then, in each handler that accesses a user-specific resource, retrieve both the authenticated identity and the requested resource ID, and verify they match (or that the authenticated user has the required role or scope).
Example: middleware that reads the certificate subject and stores the user ID in request extensions:
use axum::{extract::Extension, async_trait, body::Body, http::{Request, Response, StatusCode, HeaderMap, header},
middleware::Next};
use std::future::Future;
use std::sync::Arc;
use std::pin::Pin;
struct MyMtlsIdentity {
pub user_id: String,
}
struct MtlsMiddleware;
impl tower::Layer for MtlsMiddleware
where
F: tower::Service, Response = Response> + Clone + Send + 'static,
F::Future: Send + 'static,
F::Response: Into>,
{
type Service = MtlsMiddlewareService;
fn layer(&self, inner: F) -> Self::Service {
MtlsMiddlewareService { inner }
}
}
struct MtlsMiddlewareService<F> {
inner: F,
}
impl<F, Fut, S> tower::Service> for MtlsMiddlewareService<F>
where
F: tower::Service, Response = Response, Error = S> + Clone + Send + 'static,
F::Future: Send + 'static,
F::Response: Into>,
S: std::fmt::Debug,
{
type Response = Response<Body>;
type Error = S;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
// In practice, extract the peer certificate from the request extensions
// placed by your TLS layer (e.g., via axum-extra's ConnectInfo or a custom extractor).
// Here we simulate it with a dummy value.
let authenticated_user_id = "user-123".to_string(); // extracted from mTLS cert
req.extensions_mut().insert(Arc::new(authenticated_user_id));
let fut = self.inner.call(req);
Box::pin(async move { fut.await })
}
}
Example: handler that enforces ownership after mTLS authentication:
use axum::{routing::get, Router, Extension, extract::Path, http::StatusCode};
use std::sync::Arc;
async fn get_user_profile(
Extension(claims): Extension>, // authenticated identity from mTLS
Path(user_id): Path, // requested resource ID
) -> Result<String, (StatusCode, String)> {
if *claims != user_id {
return Err((StatusCode::FORBIDDEN, "Access denied".to_string()));
}
// Safe: the authenticated principal matches the requested resource
Ok(format!("Profile for {}", user_id))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:user_id", get(get_user_profile))
.layer(MtlsMiddleware)
.layer(Extension(Arc::new("user-123".to_string()))); // simplified for example
let addr = ([127, 0, 0, 1], 3000).into();
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
Key points:
- mTLS provides client authentication, but the server must still enforce per-resource authorization.
- Always compare the authenticated principal (from mTLS) with the resource identifier before returning data.
- Use deny-by-default logic: only allow access when the authenticated identity explicitly matches the requested resource owner.
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 |