Clickjacking in Actix with Mutual Tls
Clickjacking in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side attack where an attacker tricks a user into interacting with a hidden or disguised UI element inside an iframe. Even when you enforce Mutual Tls (mTLS) for strong identity verification between client and server, clickjacking remains possible because mTLS secures the transport and peer authentication, but does not prevent the browser from rendering a malicious page that embeds your protected Actix endpoints in invisible frames.
With an Actix web service, mTLS ensures only clients holding a valid client certificate can establish a TLS connection. This prevents unauthorized clients from reaching your endpoints, but once a legitimate client authenticates via mTLS, the server typically returns an HTML page or an API response. If that response is served with default security headers, an attacker can host a page that loads your Actix origin in an iframe and overlay interactive controls (e.g., buttons, forms) on top, capturing user actions without their knowledge.
The combination of mTLS and clickjacking highlights a common misconception: strong authentication does not imply safe embedding. Because mTLS is verified at the TLS layer before the application processes the request, an authenticated session can be hijacked visually via framing. For example, an authenticated user with a client certificate might visit a phishing site that embeds https://your-actix-api.example/dashboard; if the response lacks Content-Security-Policy frame directives and X-Frame-Options, the browser will render the content, enabling the attacker to simulate UI interactions and potentially perform unauthorized state changes.
In an Actix-based API, this risk is introduced when responses include HTML that can be framed, or when APIs are used by web clients that render responses in iframes. Even if your API primarily serves JSON, a web frontend consuming that JSON might embed third-party pages that include your origin. Without explicit anti-framing controls, mTLS protects the channel but does nothing to prevent the browser from displaying your content in a malicious context.
To assess this using middleBrick, you can run a scan against your Actix endpoint with mTLS configured on your test client. middleBrick’s checks include Input Validation and Security Header Analysis (part of its 12 parallel checks), and it will surface missing frame-protection headers even when mTLS is in place. middleBrick does not fix headers or UI framing logic; it provides prioritized findings and remediation guidance so you can implement defenses like Content-Security-Policy: frame-ancestors and X-Frame-Options.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
To mitigate clickjacking in an Actix service while retaining mTLS, you must enforce framing protections at the HTTP response level. Below are concrete code examples showing how to configure Actix to require client certificates and add anti-framing headers.
First, ensure your Actix server is set up to request and verify client certificates. In Rust using the actix-web ecosystem and openssl via actix-web-httpauth or native TLS acceptor, you can configure the server to require client certs. The example uses rustls, which is common for mTLS in Actix services:
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use actix_web::http::header;
use std::sync::Arc;
use rustls::{ServerConfig, Certificate, PrivateKey};
use rustls_pemfile::{certs, pkcs8_private_keys};
use std::io::BufReader;
use std::fs::File;
async fn index() -> impl Responder {
HttpResponse::Ok()
.insert_header((header::CONTENT_SECURITY_POLICY, "frame-ancestors 'self'"))
.insert_header((header::X_FRAME_OPTIONS, "DENY"))
.body("Hello from Actix with mTLS and anti-clickjacking headers")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Load server certificate and key
let cert_file = &mut BufReader::new(File::open("server-cert.pem").unwrap());
let key_file = &mut BufReader::new(File::open("server-key.pem").unwrap());
let cert_chain: Vec = certs(cert_file).unwrap().into_iter().map(Certificate).collect();
let mut keys: Vec = pkcs8_private_keys(key_file).unwrap().into_iter().map(PrivateKey).collect();
// Configure TLS with client certificate verification
let mut server_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth() // we will require client auth via verify_cert
.with_single_cert(cert_chain, keys.remove(0))
.expect("invalid certificate or key");
server_config.client_auth_mode = rustls::server::ClientAuthMode::Required;
server_config.root_store = Arc::new({
let mut root_store = rustls::RootCertStore::empty();
// Load trusted CA certificates that sign client certs
let mut ca_file = BufReader::new(File::open("ca-certs.pem").unwrap());
root_store.add_parsable_server_cert_verifier(Arc::new(rustls::server::AllowAnyAuthenticatedClient::new(Arc::new(root_store)))).unwrap();
root_store
});
HttpServer::new(move || {
App::new()
.route("/", web::get().to(index))
// Additional routes here
})
.bind_rustls("127.0.0.1:8443", server_config)?
.run()
.await
}
This example configures the server to require client certificates and adds two headers on each response: Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: DENY. These headers prevent browsers from embedding the page in frames, mitigating clickjacking regardless of the mTLS assurance.
If you prefer actix-web-httpauth for client certificate validation via basic auth-style extraction, you can still add the same security headers in a default handler or middleware. For a global approach, implement a response wrapper or middleware that injects frame-protection headers on every response:
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error, middleware::Next};
use actix_web::body::BoxBody;
pub struct SecurityHeadersMiddleware;
impl actix_web::middleware::Transform for SecurityHeadersMiddleware
where
S: actix_web::dev::Service, Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Transform = SecurityHeadersMiddlewareImpl;
type InitError = ();
type Future = std::future::Ready>;
fn new_transform(&self, service: S) -> Self::Future {
std::future::ready(Ok(SecurityHeadersMiddlewareImpl { service }))
}
}
pub struct SecurityHeadersMiddlewareImpl {
service: S,
}
impl actix_web::dev::Service for SecurityHeadersMiddlewareImpl
where
S: actix_web::dev::Service, Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = std::pin::Pin> + 'static>>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
res.headers_mut().insert(
header::CONTENT_SECURITY_POLICY,
header::HeaderValue::from_static("frame-ancestors 'self'"),
);
res.headers_mut().insert(
header::X_FRAME_OPTIONS,
header::HeaderValue::from_static("DENY"),
);
Ok(res)
})
}
}
By combining mTLS for strong client authentication with explicit anti-framing headers, you reduce the risk that an authenticated session can be abused in clickjacking attacks. middleBrick can validate that these headers are present and flag missing protections even when mTLS is in use.