Open Redirect in Actix with Mutual Tls
Open Redirect in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
An Open Redirect occurs when an API endpoint uses untrusted input to construct a redirect response (e.g., HTTP 302), causing the client to be sent to an arbitrary URL. In Actix web, this typically manifests through a handler that reads a next or redirect_to query parameter and passes it directly to HttpResponse::Found() or similar. When Mutual Tls (mTLS) is enforced, the server authenticates the client via a client certificate, which may create a false sense of security. Operators might assume mTLS bounds the client identity and therefore relax input validation on redirect endpoints. However, mTLS does not constrain the values a valid, authenticated client can send. An authorized client can still supply a malicious URL in a redirect parameter. Because the server already authenticated the client via certificate, it may skip strict allowlisting of the target host and perform a redirect, effectively turning the authenticated endpoint into an open redirect for that trusted client.
Moreover, in an mTLS-enabled Actix service, developers sometimes inspect the client certificate subject or common name to derive a user or tenant context, then append additional routing or redirection logic. If the redirect target is derived or influenced by client-supplied data (e.g., a return_to parameter) combined with the certificate-derived context, the validation boundary can be bypassed. For example, an attacker with a valid certificate might provide a relative path or a fully qualified external URL; if the application does not validate that the resolved host is within an allowed set, the redirect can lead to phishing or token theft. The combination of mTLS and an improperly constrained redirect parameter means the attack surface is not reduced by authentication; it shifts from “who are you” to “what are you allowed to do.”
Consider an Actix handler like the following vulnerable pattern:
use actix_web::{web, HttpResponse};
async fn redirect_handler(query: web::Query>) -> HttpResponse {
let next = query.get("next").unwrap_or(&"/".to_string());
HttpResponse::Found()
.insert_header(("Location", next.as_str()))
.finish()
}
Even with mTLS configured at the Actix server level (via rustls), this handler trusts the next parameter. An authenticated client can supply next=https://evil.com and the server will issue a 302 to that external site. The mTLS channel secures transport and client authentication, but does not mitigate the open redirect logic flaw.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
Remediation focuses on strict validation of redirect targets and avoiding any direct use of client-controlled URLs. Do not rely on transport-layer authentication to enforce business logic constraints. Always treat redirect targets as untrusted input, even when the client presents a valid certificate.
1. Validate against an allowlist of permitted hosts. Resolve the target URL and ensure its host is in a pre-approved set. Reject or default to a safe location otherwise.
2. Prefer relative paths or internal identifiers for redirects. If you must use absolute URLs, map them to internal route names and never forward raw user input to the Location header.
3. Enforce strict parsing to avoid open redirects via nested encodings or URL manipulation (e.g., https://[email protected]).
Example: Safe redirect handler in Actix with mTLS
Below is a concrete, secure implementation. It parses the next parameter, resolves it, and allows only hosts from an allowlist. It works alongside mTLS configured at the Actix server level (e.g., via rustls).
use actix_web::{web, HttpResponse, Error};
use url::Url;
use std::collections::HashSet;
// Define allowed redirect hosts
fn allowed_redirect_hosts() -> HashSet<&'static str> {
let mut set = HashSet::new();
set.insert("app.example.com");
set.insert("dashboard.example.com");
set
}
async fn safe_redirect(query: web::Query>) -> Result {
let next = query.get("next").map(|s| s.as_str()).unwrap_or("/");
let parsed = Url::parse(next).map_err(|_| actix_web::error::ErrorBadRequest("Invalid URL"))?;
let host = parsed.host_str().ok_or_else(|| actix_web::error::ErrorBadRequest("Missing host"))?;
if !allowed_redirect_hosts().contains(host) {
return Ok(HttpResponse::Found()
.insert_header(("Location", "/"))
.finish());
}
// Ensure no unexpected port or path manipulation bypass
if parsed.port().map_or(false, |p| p != 443 && p != 80) {
return Ok(HttpResponse::Found()
.insert_header(("Location", "/"))
.finish());
}
Ok(HttpResponse::Found()
.insert_header(("Location", next))
.finish())
}
Example: mTLS configuration in Actix (server side)
Here is a typical server setup using rustls for mTLS. Note that the redirect handler above remains separate and does not rely on the certificate for authorization of the target URL.
use actix_web::{App, HttpServer};
use actix_web::middleware::Logger;
use std::sync::Arc;
use rustls::{ServerConfig, Certificate, PrivateKey};
use std::io::BufReader;
use std::fs::File;
// Load server certificate and key
let mut config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth() // We will require client auth via verify_client_cert
.with_single_cert(
load_certs("server-cert.pem"),
load_private_key("server-key.pem"),
)
.map_err(|err| panic!("Failed to load server cert: {:?}", err))?;
// Configure client certificate verification
config.client_auth_root_subjects = load_trust_anchors();
config.verify_client_cert = Some(Arc::new(|certs| {
// Custom verification: ensure client cert is present and optionally check OID mappings
!certs.is_empty()
}));
async fn load_certs(path: &str) -> Vec {
let certfile = &mut BufReader::new(File::open(path).unwrap());
rustls_pemfile::certs(certfile).unwrap().into_iter().map(Certificate).collect()
}
async fn load_private_key(path: &str) -> PrivateKey {
let keyfile = &mut BufReader::new(File::open(path).unwrap());
let keys = rustls_pemfile::pkcs8_private_keys(keyfile).unwrap();
PrivateKey(keys[0].clone())
}
async fn load_trust_anchors() -> rustls::client::ClientCertVerifier {
// In practice, use rustls::RootCertStore and configure verifier
// This is a simplified placeholder
Arc::new(|_end_entity, _intermediates| Ok(()))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.service(web::resource("/redirect").to(safe_redirect))
})
.bind_rustls(
"0.0.0.0:8443",
config,
)?
.run()
.await
}