Log Injection in Actix with Mutual Tls
Log Injection in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
Log injection occurs when untrusted input is written directly into application logs without validation or sanitization. In Actix web applications using mutual TLS (mTLS), the presence of client certificates and additional request metadata can introduce new loggable fields that, if improperly handled, become injection vectors.
With mTLS enabled, Actix can surface fields such as the client certificate subject, issuer, serial number, and Common Name (CN). These values are often interpolated into logs for audit or debugging purposes. If an attacker can influence any part of the TLS handshake or the data derived from it (for example, by providing a malicious CN containing newline or control characters), they can inject log entries that forge timestamps, inject structured log delimiters, or append additional lines. This can corrupt log-based monitoring, facilitate log forging attacks, or assist in bypassing detection rules that rely on log integrity.
Consider a typical Actix logging pattern that includes the client CN:
use actix_web::{web, App, HttpServer, Responder};
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
fn main() -> std::io::Result<()> {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
builder.set_certificate_chain_file("cert.pem").unwrap();
builder.set_client_ca_file("ca.pem").unwrap();
builder.set_verify(openssl::ssl::SslVerifyMode::PEER | openssl::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT, verify_callback);
HttpServer::new(move || {
App::new()
.wrap(actix_web_httpauth::middlewares::HttpAuthentication::basic(|_req, credentials| {
// Basic auth example alongside mTLS
async { Ok(true) }
}))
.route("/", web::get().to(|| async { "ok" }))
})
.bind_openssl("127.0.0.1:8443", builder)?
.run()
}
fn verify_callback(
cert: &openssl::x509::X509Certificate,
_: &mut openssl::pkey::PKeyRef<openssl::pkey::Public>,
) -> bool {
let subject = cert.subject_name();
let cn_index = subject.entries().find(|e| e.object().to_string() == "2.5.4.3");
if let Some(entry) = cn_index {
if let Ok(cn) = entry.data().as_utf8_string() {
// Potentially unsafe logging if cn contains newlines or control chars
println!("Client CN: {}", cn);
}
}
true
}
If cn contains characters such as \n or \r, the log line can be split or appended to, enabling log injection. Additionally, combining mTLS with other contextual data (e.g., request IDs, authorization details) increases the number of loggable surfaces. Attack patterns include forging elevated privilege entries or obscuring real attack traces by injecting structured log tokens.
Log injection in this context does not exploit a flaw in TLS itself, but rather insecure logging practices around mTLS-derived metadata. The risk is higher when logs are consumed by automated systems that parse structure (e.g., key=value or JSON) and when audit trails are used for compliance. mTLS setups often emphasize strong identity, which can inadvertently encourage logging identity fields without sufficient sanitization.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
Remediation focuses on strict input validation and canonicalization of any data derived from the TLS handshake before it reaches log statements. In Actix, handle certificate fields defensively by sanitizing strings and avoiding direct interpolation of raw identity values into logs.
First, sanitize the CN by removing or replacing control characters and newlines:
use actix_web::{web, App, HttpServer, Responder};
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslVerifyMode};
fn main() -> std::io::Result<()> {
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
builder.set_certificate_chain_file("cert.pem").unwrap();
builder.set_client_ca_file("ca.pem").unwrap();
builder.set_verify(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT, verify_callback);
HttpServer::new(move || {
App::new().route("/", web::get().to(|| async { "ok" }))
})
.bind_openssl("127.0.0.1:8443", builder)?
.run()
}
fn sanitize_log_value(value: &str) -> String {
value.chars().filter(|c| !c.is_control()).collect::().replace('\n', "\\n").replace('\r', "\\r")
}
fn verify_callback(
cert: &openssl::x509::X509Certificate,
_: &mut openssl::pkey::PKeyRef<openssl::pkey::Public>,
) -> bool {
let subject = cert.subject_name();
let cn_index = subject.entries().find(|e| e.object().to_string() == "2.5.4.3");
if let Some(entry) = cn_index {
if let Ok(cn) = entry.data().as_utf8_string() {
let safe_cn = sanitize_log_value(&cn);
println!("Client CN: {}", safe_cn);
}
}
true
}
Second, structure log entries as JSON and encode values to prevent injection of delimiters:
use serde_json::json;
fn verify_callback(
cert: &openssl::x509::X509Certificate,
_: &mut openssl::pkey::PKeyRef<openssl::pkey::Public>,
) -> bool {
let subject = cert.subject_name();
let cn_index = subject.entries().find(|e| e.object().to_string() == "2.5.4.3");
let mut log_obj = serde_json::Map::new();
log_obj.insert("event".to_string(), json!("tls_client_verify"));
if let Some(entry) = cn_index {
if let Ok(cn) = entry.data().as_utf8_string() {
let safe_cn = sanitize_log_value(&cn);
log_obj.insert("client_cn".to_string(), json!(safe_cn));
}
}
println!("{}", serde_json::to_string(&log_obj).unwrap_or_default());
true
}
These practices reduce the risk of log injection while preserving useful audit information. They complement broader secure coding measures such as structured logging frameworks and centralized log processing that validates input at ingestion.