Unicode Normalization in Axum with Mutual Tls
Unicode Normalization in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
When an Axum service is protected with Mutual Transport Layer Security (mTLS), client authentication occurs at the TLS layer before HTTP semantics are inspected. mTLS ensures the identity of both client and server, but it does not normalize or validate the Unicode representation of identifiers such as paths, headers, or JWT claims. This mismatch means an attacker can supply a carefully crafted Unicode string that is semantically equivalent to an authorized resource after normalization but different in bytes before processing.
In Axum, routing often depends on path parameters or headers that are matched directly or used as keys. If an application receives a request with a decomposed Unicode form (e.g., NFC vs NFD), the route matcher might evaluate differently than the developer expects. For example, a path like /user/é/profile can appear as either a single precomposed code point or as e followed by a combining acute accent. Without normalization, these two byte sequences may map to different route handlers or different authorization checks, enabling BOLA/IDOR-like confusion where one user’s normalized resource appears accessible through another’s raw input.
Mutual Tls adds a layer of identity, but it does not mitigate injection or confusion attacks at the application layer. If an endpoint uses the raw, unnormalized string for data lookup or permission checks, an attacker can exploit canonicalization gaps. Consider an endpoint that maps /files/{id} where {id} is compared against a database key after TLS authentication. A Unicode-normalization-based IDOR can occur when two different representations resolve to the same logical identifier; the server may inadvertently serve data belonging to another authenticated peer because the comparison was not performed on a normalized form.
Compounded by mTLS, the server trusts the authenticated peer but may still process identifiers inconsistently. An authorized client could send a request with a normalized certificate subject distinguished name (e.g., common name or organization fields) while also providing a non-normalized path or header. If the application mixes these inputs without canonicalization, it may inadvertently apply different authorization logic to equivalent values. This can lead to privilege escalation or information exposure when combined with Property Authorization or BOLA checks that rely on raw input rather than normalized values.
To detect such issues, scanning with middleBrick is valuable because it runs 12 security checks in parallel, including Input Validation and Authorization. It compares OpenAPI/Swagger specs with runtime behavior, highlighting mismatches where path or header handling may not account for Unicode equivalence. The scanner’s LLM/AI Security module can also probe for subtle injection chains that arise from normalization bypasses, ensuring that mTLS-protected endpoints are evaluated for canonicalization flaws as rigorously as for cryptographic configuration.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
Remediation centers on normalizing identifiers before they are used in routing, authorization, or data access. For paths and parameters, apply Unicode normalization (NFC is a common, stable choice) before comparison or storage. For headers and JWT claims, normalize values after TLS-bound authentication but before any business logic. This ensures that equivalent Unicode representations are treated identically regardless of the client’s input form.
Below is a concrete Axum example implementing normalization in middleware for mTLS-protected routes. The middleware runs after TLS client authentication has occurred and normalizes both path parameters and selected headers before they reach your handlers.
use axum::{
async_trait,
extract::{self, FromRequestParts},
http::{request::Parts, HeaderValue, StatusCode},
};
use std::convert::Infallible;
use unicode_normalization::UnicodeNormalization;
/// A typed extract that provides a normalized version of a path parameter.
pub struct NormalizedPath(pub T);
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for NormalizedPath<String> {
type Rejection = (StatusCode, String);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result {
// `path` is expected to be set by Axum's Path extractor earlier.
// Here we demonstrate normalization for a captured segment.
let raw = parts
.extensions
.get::<String>()
.cloned()
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "missing path".to_string()))?;
// Normalize to NFC to ensure canonical representation.
let normalized: String = raw.nfc().collect();
Ok(NormalizedPath(normalized))
}
}
/// A header extractor that normalizes a specific header value.
pub struct NormalizedHeader(pub String);
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for NormalizedHeader {
type Rejection = (StatusCode, String);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let header_value = parts
.headers
.get("X-Resource-Id")
.ok_or((StatusCode::BAD_REQUEST, "missing X-Resource-Id header"))?
.to_str()
.map_err(|_| (StatusCode::BAD_REQUEST, "invalid header encoding"))?
.to_string();
// Normalize header value before using it for authz or lookup.
let normalized: String = header_value.nfc().collect();
Ok(NormalizedHeader(normalized))
}
}
/// Example route using both extractors.
async fn get_resource(
path: NormalizedPath<String>,
header: NormalizedHeader,
) -> String {
// Use normalized values for routing or data access.
format!("path={}, header={}", path.0, header.0)
}
/// Build your app with mTLS and normalized extractors.
/// Assume TLS configuration with `tls_layer` enforcing client certs.
async fn build_app() -> axum::Router {
use axum::routing::get;
use tower_http::set_header::SetResponseHeaderLayer;
axum::Router::new()
.route("/files/:id", get(get_resource))
.layer(SetResponseHeaderLayer::overriding(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
))
// Add your mTLS layer here, e.g., via `tls_layer`.
}
This pattern ensures that comparisons involving paths or headers use a consistent canonical form. For JWTs, normalize claims like sub or custom identifiers before mapping them to internal identifiers. In storage, persist normalized keys so that lookups remain consistent across clients.
middleBrick’s CLI can validate that your routes and expected inputs align with normalization expectations. By scanning with the CLI — middlebrick scan https://your-api.example.com — you can observe how the tool’s Input Validation and Authorization checks surface mismatches. The Dashboard and GitHub Action integrations further allow you to enforce that no regression in identifier handling reaches production, complementing mTLS by focusing on application-level canonicalization rather than transport identity.