Hallucination Attacks in Axum with Mutual Tls
Hallucination Attacks in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
In an Axum service that enforces mutual TLS (mTLS), a hallucination attack can occur when a client presents a valid certificate but is not authorized to access specific resources or to invoke certain endpoints. The server validates the TLS handshake and may assign an identity (e.g., subject or certificate hash) to the request, but authorization logic is missing or insufficient. This mismatch between authentication (mTLS) and authorization creates a path for attackers to "hallucinate" access to data or operations they should not reach. Because mTLS ensures transport-layer identity, developers can mistakenly assume that authentication equals authorization, leading to missing checks at the application layer.
An attacker with a legitimate certificate might probe unauthenticated or weakly protected endpoints, testing for IDOR or BOLA-style behavior. Even though the TLS layer provides identity, Axum routes will process requests according to the handler logic; if handlers do not validate scopes, roles, or tenant context, the request proceeds as if the client is authorized. The risk is compounded when handlers trust metadata derived from the certificate (e.g., CN or SAN) without additional constraints. For example, a handler might use certificate fields to build a user context but fail to verify that the user owns the target resource. This allows the attacker to hallucinate access by iterating through IDs or guessing relationships. Input validation and rate limiting remain essential because mTLS does not sanitize or constrain the content of the request itself.
LLM/AI security considerations appear when services expose endpoints that return generated content or system prompts. If an mTLS-protected endpoint leaks system prompts or exposes model behavior via crafted inputs, attackers can use prompt injection techniques adapted to the API surface. While mTLS secures the channel, it does not prevent adversarial inputs designed to elicit hallucinated responses or to probe for excessive agency in downstream LLM integrations. Data exposure can occur if responses include sensitive information tied to the authenticated certificate’s identity but intended for a different context. Therefore, Axum services must couple mTLS with explicit authorization checks, scoped policies, and input validation to prevent hallucination attacks across transport identity, application logic, and AI security dimensions.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
Remediation requires enforcing authorization after mTLS authentication in Axum. Use extractor patterns to retrieve peer identity from the request extensions populated by the TLS layer, then validate permissions before processing. Below are concrete, working examples that combine mTLS setup with authorization guards.
1. mTLS configuration with rustls and Axum extractors
use axum::Router;
use std::net::SocketAddr;
use tokio_rustls::rustls::{ServerConfig, Certificate, PrivateKey};
use tokio_rustls::TlsAcceptor;
use std::sync::Arc;
async fn build_router() -> Router {
Router::new()
.route("/api/resource/:id", axum::routing::get(handle_resource))
}
#[tokio::main]
async fn main() {
let certs = load_certs("certs/server.crt").expect("failed to load cert");
let key = load_private_key("certs/server.key").expect("failed to load key");
let config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth() // we will handle auth manually after TLS
.with_single_cert(certs, key)
.expect("invalid cert or key");
let acceptor = TlsAcceptor::from(Arc::new(config));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8443").await.unwrap();
loop {
let (stream, _) = listener.accept().await.unwrap();
let acceptor = acceptor.clone();
tokio::spawn(async move {
let tls_stream = acceptor.accept(stream).await.unwrap();
// Extract peer cert for identity
let peer_certs = tls_stream.get_ref().1.get_peer_certificates();
// You can store peer_certs in request extensions here via a layer
});
}
}
fn load_certs(path: &str) -> std::io::Result<Vec<Certificate>> {
let certfile = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(certfile);
rustls_pemfile::certs(&mut reader).collect()
}
fn load_private_key(path: &str) -> std::io::Result<PrivateKey> {
let keyfile = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(keyfile);
let keys = rustls_pemfile::pkcs8_private_keys(&mut reader)?;
keys.into_iter().next().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "no private key"))
}
2. Axum extractor that validates authorization after mTLS
use axum::async_trait;
use axum::extract::{Extension, FromRequest};
use axum::http::request::Parts;
use std::convert::Infallible;
use std::sync::Arc;
struct MtlsIdentity {
pub subject: String,
pub scopes: Vec<String>,
}
struct AuthorizedResource {
pub user_id: String,
}
#[async_trait]
impl FromRequest<S> for AuthorizedResource
where
S: Send + Sync,
{
type Rejection = (axum::http::StatusCode, String);
async fn from_request(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Retrieve identity from request extensions (populated by a middleware layer)
let identity = req.extensions.get::<MtlsIdentity>()
.ok_or_else(|| (axum::http::StatusCode::FORBIDDEN, "missing identity".to_string()))?;
// Example: ensure the identity has a required scope
if !identity.scopes.contains("read:resource".to_string()) {
return Err((axum::http::StatusCode::FORBIDDEN, "insufficient scope".to_string()));
}
// Example: enforce ownership or tenant check (BOLA/IDOR prevention)
let resource_id = req.uri.path().split("/").last().unwrap_or("");
// Replace with actual lookup: does this identity own this resource?
if !is_authorized_resource(&identity.subject, resource_id) {
return Err((axum::http::StatusCode::FORBIDDEN, "access to resource denied".to_string()));
}
Ok(AuthorizedResource { user_id: identity.subject.clone() })
}
}
fn is_authorized_resource(subject: &str, resource_id: &str) -> bool {
// Implement your policy: e.g., check a mapping subject -> owned resources
subject == resource_id // simplistic example
}
async fn handle_resource(req: axum::http::Request<axum::body::Body>) -> axum::http::Response<String> {
// If AuthorizedResource was extracted successfully, proceed
axum::http::Response::new("secure resource data".to_string())
}
3. Middleware to populate identity from client cert
use axum::middleware::{self, Next};
use axum::extract::Request;
use axum::http::Response;
use std::sync::Arc;
async fn mtls_identity_middleware(
request: Request,
next: Next,
) -> Response {
// Extract peer certs from request extensions (set by the TLS layer)
let peer_certs = request.extensions().get::<Vec<Vec<u8>>>()
.cloned()
.unwrap_or_default();
let identity = if let Some(certs) = peer_certs.first() {
// Parse subject DN; use `openssl` or `webpki` in production
parse_subject_from_cert(certs)
} else {
"unknown".to_string()
};
let scopes = vec!["read:resource".to_string()];
let identity = MtlsIdentity { subject: identity, scopes };
let request = request.with_extensions(Arc::new(identity));
next.run(request).await
}
fn parse_subject_from_cert(cert_der: &[u8]) -> String {
// Placeholder: implement proper parsing with `x509-parser` or `openssl`
"CN=alice,O=example".to_string()
}
4. Complementary protections
Combine mTLS with input validation, rate limiting, and explicit authorization checks. mTLS does not prevent over-posting, injection, or LLM-specific risks such as prompt injection or data exfiltration through model outputs. Apply schema validation on request bodies, enforce per-endpoint rate limits, and scope data access by identity to mitigate BOLA/IDOR and hallucination-style abuses across both traditional API misuse and AI-integrated endpoints.
Related CWEs: llmSecurity
| CWE ID | Name | Severity |
|---|---|---|
| CWE-754 | Improper Check for Unusual or Exceptional Conditions | MEDIUM |