Unicode Normalization in Actix with Mutual Tls
Unicode Normalization in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
Unicode normalization issues arise when an API compares user-supplied strings after normalization differently than it processes normalized values stored or compared internally. In Actix applications that also enforce mutual TLS (mTLS), the combination of per-request client certificate identity and string handling can amplify risk: the same logical identity (e.g., a principal derived from certificate fields) may be compared after normalization in application logic but before normalization in security checks, leading to bypasses or privilege confusion.
Mutual TLS binds a client identity to the TLS layer, and Actix typically extracts principal information (such as a subject common name or a SAN) from the certificate presented by the client. If that extracted value is later compared with stored identifiers or used in authorization decisions, inconsistent normalization can be introduced. For example, a client certificate may include an emailAddress field containing Unicode characters (e.g., josé@example.com). If the application normalizes this to NFC before comparison but compares against a normalized database value using a different normalization form (e.g., NFD or NFKC), an attacker could present a certificate with a canonically equivalent but differently encoded string to bypass identity-based checks.
Another scenario involves path or header values influenced by client certificate metadata. An attacker could use combining characters or different Unicode representations to create an input that appears identical visually but differs in code point composition. Without normalization applied consistently before comparison, an Actix route that relies on certificate-derived claims for authorization may inadvertently grant access to a different principal. The risk is especially relevant when mTLS is used for authentication and the application performs additional checks (such as mapping certificate fields to roles) using string operations that do not enforce a canonical normalization form.
These issues map onto the broader OWASP API Top 10 category of Broken Object Level Authorization (BOLA) and can interact with mTLS by subverting identity-based controls. A scanner that understands both runtime behavior and OpenAPI contracts—such as one that correlates spec definitions with observed findings—can detect dangerous patterns where normalization-sensitive operations intersect with mTLS-bound principals.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
To mitigate Unicode normalization issues in Actix with mutual TLS, normalize all certificate-derived strings consistently before using them in comparisons, authorization, or identity mapping. Use a well-tested Unicode normalization library to convert to a single canonical form (typically NFC) at the point where certificate data is first extracted, and apply the same normalization to any stored values before comparison.
Below are concrete Actix examples that show how to integrate normalization into an mTLS-based extractor and authorization flow. These examples assume you have configured TLS with client verification and have access to peer certificates via Actix’s certificate extraction APIs.
Example 1: Normalizing a certificate emailAddress claim
use actix_web::{web, HttpRequest, Error};
use unicode_normalization::UnicodeNormalization;
/// Extract the client certificate emailAddress and normalize to NFC.
fn normalized_cert_email(req: &HttpRequest) -> Option {
req.extensions()
.get::() // Assume certificate email stored as String in extensions
.map(|email| email.nfc().collect())
}
async fn profile(req: HttpRequest) -> Result {
if let Some(email) = normalized_cert_email(&req) {
// Compare against a pre-normalized stored value
let stored = "josé@example.com".nfc().collect::();
if email == stored {
return Ok("Access granted".into());
}
}
Err(actix_web::error::ErrorUnauthorized("invalid certificate"))
}
Example 2: Normalizing a certificate common name for role mapping
use actix_web::{HttpRequest, Error};
use unicode_normalization::UnicodeNormalization;
/// Normalize CN to NFC and map to roles.
fn role_for_certificate(req: &HttpRequest) -> Option<&'static str> {
let cn = req.extensions().get::()?; // CN extracted and stored in extensions
let cn_normalized: String = cn.nfc().collect();
match cn_normalized.as_str() {
"[email protected]" => Some("admin"),
"[email protected]" => Some("user"),
_ => None,
}
}
async fn restricted(resource: web::Path, req: HttpRequest) -> Result {
if let Some(role) = role_for_certificate(&req) {
if role == "admin" {
return Ok(format!("Admin access to {}", resource));
}
}
Err(actix_web::error::ErrorForbidden("insufficient permissions"))
}
Example 3: Centralized extractor to enforce normalization once
use actix_web::{dev::Payload, FromRequest, HttpRequest, Error, web};
use futures::future::{ok, Ready};
use unicode_normalization::UnicodeNormalization;
struct NormalizedCertPrincipal(String);
impl FromRequest for NormalizedCertPrincipal {
type Error = Error;
type Future = Ready>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let principal = req.extensions()
.get::()
.map(|s| s.nfc().collect())
.unwrap_or_default();
ok(Ok(NormalizedCertPrincipal(principal)))
}
}
async fn handler(principal: NormalizedCertPrincipal) -> String {
// principal.0 is guaranteed to be NFC-normalized
format!("Processing for: {}", principal.0)
}
Additional practical guidance:
- Apply normalization at the boundary where certificate data enters your application (e.g., during TLS handshake extraction or immediately after) so that all downstream comparisons use a consistent form.
- Ensure that any user-controlled or configuration strings that may be concatenated or compared with certificate-derived values are also normalized to the same form.
- Store normalized values in databases and caches to avoid repeated runtime normalization and reduce inconsistency risk.
- Test equivalence classes using strings with combining marks, case differences, and full-width/half-width characters to confirm normalization behaves as expected across your toolchain.
By normalizing certificate-derived identifiers consistently, you preserve the security intent of mutual TLS while preventing Unicode-based bypasses that exploit comparison mismatches.