Clickjacking in Axum with Mutual Tls
Clickjacking in Axum 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. In Axum, even when Mutual TLS (mTLS) is enforced for strong peer authentication, clickjacking remains possible because mTLS protects the channel, not the rendered UI. mTLS ensures the client is a known, authorized peer, but it does not prevent a malicious site from embedding your Axum-served UI in an invisible frame.
When an Axum backend serves HTML or JSON that is intended to be consumed by a trusted frontend, mTLS verifies the client’s identity via client certificates. However, if the responses do not include anti-clickjacking defenses, an attacker can host a page that loads your endpoint in a transparent iframe and overlay interactive elements (buttons, links) to capture actions. Because mTLS is often used in internal or high-trust environments, developers may assume the UI is safe from framing, which creates a false sense of security.
The combination of mTLS and missing frame protections is particularly risky in scenarios where the Axum server serves admin panels or sensitive operations (e.g., confirmations, fund transfers). The server validates the client certificate, but if the page is clickjacked, the authorized client performs unintended actions within the attacker’s page. Since mTLS does not restrict how the response is embedded, the server must explicitly enforce frame-ancestor policies and other UI-level protections.
Additionally, if Axum APIs return JSON that is rendered by a frontend framework, clickjacking can still occur via UI manipulation even when mTLS secures backend calls. For example, an attacker could embed the API response inside a hidden form and simulate user input. The mTLS layer remains intact, but the application logic is compromised through social engineering and UI deception. Therefore, developers must treat clickjacking as an application-layer issue and implement explicit defenses regardless of transport security.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
To mitigate clickjacking in Axum while using Mutual TLS, you must enforce frame-ancestor policies and ensure that responses are not embeddable in untrusted contexts. This is done via HTTP headers and careful UI design, independent of mTLS configuration.
1. Set Content-Security-Policy frame-ancestors
The most effective defense is the Content-Security-Policy header with frame-ancestors restricted to trusted origins or 'none' for sensitive pages.
use axum::response::{IntoResponse, Response};
use axum::http::header::CONTENT_SECURITY_POLICY;
fn with_csp_header(response: Response) -> Response {
response
.map(|body| (body))
.with_header(
CONTENT_SECURITY_POLICY,
"frame-ancestors 'self' https://trusted.example.com",
)
}
For endpoints that should never be framed, use:
response.with_header(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'")
2. Explicitly set X-Frame-Options
Although CSP frame-ancestors is modern and flexible, you may also set X-Frame-Options for broader compatibility, especially if supporting legacy browsers.
use axum::response::Response;
use axum::http::header::X_FRAME_OPTIONS;
fn with_x_frame_options(response: Response) -> Response {
response.with_header(X_FRAME_OPTIONS, "DENY")
}
3. Combine mTLS route guards with header middleware
Ensure that mTLS authentication is applied before serving responses, and that security headers are added consistently across protected routes.
use axum::{routing::get, Router};
use axum::extract::State;
use std::net::SocketAddr;
struct AppState;
async fn secure_page(State(_state): State) -> String {
"Sensitive UI with mTLS and clickjacking protections".to_string()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/admin", get(secure_page))
.layer(axum::middleware::from_fn_with_state(
AppState,
|State(state), request, next| async move {
let mut response = next.run(request).await;
response.headers_mut().insert(
"Content-Security-Policy",
"frame-ancestors 'self'".parse().unwrap(),
);
Ok(response)
},
));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
4. Validate Origin for sensitive actions
For critical operations, validate the Origin or Referer header server-side, even when mTLS is used, to ensure requests originate from your trusted frontend.
async fn validate_origin(
headers: &axum::http::HeaderMap,
) -> bool {
if let Some(origin) = headers.get("Origin") {
origin == "https://trusted.example.com"
} else {
false
}
}