Broken Access Control in Axum with Mutual Tls
Broken Access Control in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when an API fails to enforce proper authorization checks, allowing one user to access or modify resources belonging to another. In Axum, enabling Mutual Transport Layer Security (mTLS) confirms the client’s identity via a client certificate, but it does not automatically enforce application-level permissions. Relying solely on mTLS for authorization is a common misconception that can lead to BOLA/IDOR and BFLA/Privilege Escalation when role or scope claims are missing or not validated in route handlers.
For example, an mTLS-authenticated request may present a valid certificate, but if the handler does not verify the associated user ID or group, an attacker who possesses a client certificate can iterate over predictable resource identifiers (e.g., /users/123, /users/124) and access or modify data without explicit authorization checks. Axum extractors can inadvertently expose internal identifiers or accept user-supplied path parameters without confirming that the authenticated principal is permitted to operate on that resource.
The 12 parallel security checks in middleBrick specifically test for BOLA/IDOR and BFLA/Privilege Escalation, including Property Authorization, to detect these gaps. When OpenAPI/Swagger specs define scopes or roles and runtime requests do not align with those defined authorizations, middleBrick correlates spec definitions with observed behavior to highlight missing authorization enforcement. This is especially important when mTLS is used for authentication but application-level RBAC or ABAC rules are absent or inconsistently applied.
In Axum, common root causes include missing role extraction from certificate extensions or OIDC claims, lack of resource ownership checks, and over-permissive route designs that assume mTLS alone suffices for authorization. Without explicit checks, sensitive endpoints can be reached by any mTLS client, leading to data exposure and compliance violations mapped to OWASP API Top 10 A1: Broken Access Control.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
To secure Axum APIs with mTLS while preventing Broken Access Control, treat mTLS as authentication and implement explicit authorization checks for every handler. Use extractor guards to validate that the authenticated principal has the required role or scope to access the resource. Below are concrete, working examples that combine mTLS authentication with per-route authorization.
1. Configure TLS with client certificate verification
Ensure your Axum server requires and validates client certificates. The TLS acceptor configuration is typically handled outside Axum (e.g., via hyper or tower), but the application must read the presented certificate details and map them to an identity.
// src/main.rs
use axum::routing::get;
use axum::Router;
use std::net::SocketAddr;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
fn make_ssl_acceptor() -> SslAcceptor {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls_server()).unwrap();
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
builder.set_certificate_chain_file("cert.pem").unwrap();
builder.set_client_ca_list_file("ca.pem");
builder.set_verify(openssl::ssl::SslVerifyMode::PEER | openssl::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT);
builder.build()
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/api/users/me", get(handler_me));
let addr = SocketAddr::from(([127, 0, 0, 1], 8443));
let listener = tokio_rustls::TlsAcceptor::from(Arc::new(make_ssl_acceptor()));
axum::serve(listener, app).await.unwrap();
}
2. Extract identity from client certificate in Axum extractors
Create an extractor that reads the peer certificate and maps it to a user ID or scopes. This keeps handlers explicit about what identity information is available.
// src/extractors.rs
use axum::async_trait;
use axum::extract::{FromRequest, Request};
use axum::http::StatusCode;
use openssl::x509::X509;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
pub user_id: String,
pub scopes: Vec,
}
#[derive(Debug)]
pub enum AuthError {
MissingCertificate,
InvalidCertificate,
MissingScope,
}
impl std::fmt::Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthError::MissingCertificate => write!(f, "Missing certificate"),
AuthError::InvalidCertificate => write!(f, "Invalid certificate"),
AuthError::MissingScope => write!(f, "Missing required scope"),
}
}
}
impl std::error::Error for AuthError {}
impl From for StatusCode {
fn from(_: AuthError) -> Self {
StatusCode::FORBIDDEN
}
}
pub struct AuthExtractor;
#[async_trait]
impl FromRequest for AuthenticatedUser
where
S: Send + Sync,
{
type Rejection = AuthError;
fn from_request(req: Request, _state: &S) -> Pin> + Send + '_>> {
let cert = req.extensions()
.get::>()
.and_then(|certs| certs.first())
.ok_or(AuthError::MissingCertificate)?;
// Example: extract user ID from SAN or common name
let user_id = cert.subject_name().entries_by_nid(openssl::nid::Nid::COMMONNAME)
.next()
.and_then(|entry| entry.data().as_utf8_str().ok())
.map(|s| s.to_string())
.ok_or(AuthError::InvalidCertificate)?;
// Example: extract scopes from extended key usage or custom OID
let scopes = vec!["read:users".to_string()]; // derive from cert extensions in practice
Box::pin(async move { Ok(AuthenticatedUser { user_id, scopes }) })
}
}
3. Enforce authorization in handlers using role/scope checks
Never assume mTLS provides sufficient authorization. Explicitly check scopes or roles before performing operations.
// src/handlers.rs
use axum::response::IntoResponse;
use axum::Json;
use crate::extractors::AuthenticatedUser;
pub async fn handler_me(user: AuthenticatedUser) -> impl IntoResponse {
// Ensure the user has the required scope for this endpoint
if !user.scopes.contains(&"read:users".to_string()) {
return (StatusCode::FORBIDDEN, "insufficient_scope").into_response();
}
// Proceed with business logic, using user.user_id to scope data access
Json(serde_json::json!({ "user_id": user.user_id }))
}
pub async fn handler_update_profile(
user: AuthenticatedUser,
Path(target_user_id): Path,
) -> impl IntoResponse {
// BOLA protection: ensure the authenticated user can only update their own profile
if user.user_id != target_user_id {
return (StatusCode::FORBIDDEN, "access_denied").into_response();
}
// Proceed with update
StatusCode::OK
}
4. Align OpenAPI/Swagger definitions with runtime authorization
Define scopes and required roles in your OpenAPI spec and validate at build or scan time that handlers enforce them. middleBrick can correlate spec-defined security schemes with runtime behavior to highlight missing checks.
# openapi.yaml
openapi: 3.0.3
info:
title: Example API
version: 1.0.0
paths:
/api/users/me:
get:
summary: Get current user
security:
- mutualTls: [read:users]
responses:
'200':
description: OK
components:
securitySchemes:
mutualTls:
type: mutualTls
description: mTLS with scope-based authorization
x-scopes:
read:users: Read user data
write:users: Modify user data