HIGH insecure direct object referenceaxummutual tls

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 IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Does mTLS eliminate the need for IDOR checks in Axum APIs?
No. Mutual TLS authenticates the client to the server but does not enforce that the authenticated client is authorized to access a specific resource. You must still verify that the requested resource (e.g., a user ID in the path) belongs to the authenticated principal to prevent IDOR/BOLA.
What should I verify in Axum handlers after mTLS authentication to prevent BOLA/IDOR?
In each handler that accesses a resource by ID, retrieve the authenticated identity from request extensions (set by mTLS middleware) and compare it to the requested resource ID. Return 403 if they do not match, ensuring users can only access their own data.