Replay Attack in Actix with Mutual Tls
Replay Attack in Actix with Mutual Tls — how this specific combination creates or exposes the vulnerability
A replay attack in Actix with Mutual TLS (mTLS) occurs when an adversary intercepts a valid client certificate and the TLS-encrypted request (including headers and payload) and retransmits it to the server to produce the same effect as the original legitimate action. Even though mTLS provides strong server authentication and transport integrity, it does not automatically prevent replay unless additional protections are added at the application layer.
In Actix, this risk arises because mTLS authenticates the client to the server based on the certificate presented and the successful completion of the TLS handshake, but it does not bind the request semantics to a per-request unique value. Without nonces, timestamps, or cryptographic uniqueness, an intercepted request can be replayed within the certificate’s validity window. The server sees a valid mTLS connection and may process the request as intended by the legitimate client, leading to unauthorized operations such as fund transfers, state changes, or resource creation.
For example, consider an Actix web service that accepts POST /transfer with an idempotency key in a custom header. If the client authenticates with mTLS and sends Idempotency-Key: 7a3f and Transfer-Id: 123, an attacker capturing this encrypted TLS traffic can replay the same request later. The server validates the client certificate and processes the transfer again, because the application layer does not track whether idempotency-key 7a3f has already been consumed. This shows that mTLS provides transport security but not semantic request uniqueness required to defeat replay.
Other contributing factors specific to mTLS setups include lack of strict time windows on certificate validity, missing one-time-use tokens, and insufficient enforcement of idempotency at endpoints. Without these, mTLS alone cannot mitigate replay. The scanner categories in middleBrick relevant here include Authentication, Input Validation, and Unsafe Consumption, which help surface missing nonce/timestamp controls and improper handling of replay-sensitive inputs.
Mutual Tls-Specific Remediation in Actix — concrete code fixes
To prevent replay in Actix with mTLS, bind each request to a unique, single-use value and validate it server-side. Combine mTLS with idempotency keys, nonce tracking, and short time windows. Below are concrete, syntactically correct Actix examples that show mTLS setup and replay-safe handling.
1. Actix mTLS server setup (rustls)
use actix_web::{web, App, HttpServer, Responder, HttpRequest};
use actix_web::http::header::HeaderValue;
use std::sync::{Arc, Mutex};
use std::collections::HashSet;
// In-memory store for consumed idempotency keys (in production use Redis or similar)
struct AppState {
seen_keys: Mutex>,
}
async fn transfer(
req: HttpRequest,
body: web::Json,
data: web::Data>,
) -> impl Responder {
// Extract idempotency key from headers (required for replay protection)
let idempotency_key = match req.headers().get("Idempotency-Key") {
Some(v) => v.to_str().unwrap_or("").to_string(),
None => return actix_web::error::ErrorBadRequest("missing Idempotency-Key"),
};
// Reject replays: ensure key is unique per logical operation
let mut seen = data.seen_keys.lock().unwrap();
if !seen.insert(idempotency_key.clone()) {
return actix_web::error::ErrorConflict("duplicate idempotency key");
}
// Process transfer (business logic)
actix_web::web::Json(serde_json::json!({
"status": "processed",
"transfer_id": body.transfer_id,
"idempotency_key": idempotency_key,
}))
}
#[derive(serde::Deserialize)]
struct TransferRequest {
transfer_id: String,
amount: u64,
// other fields
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = Arc::new(AppState {
seen_keys: Mutex::new(HashSet::new()),
});
// Configure server with client certificate verification (mTLS)
let server = HttpServer::new(move || {
App::new()
.app_data(web::Data::new(state.clone()))
.route("/transfer", web::post().to(transfer))
})
.bind_openssl(
"127.0.0.1:8443",
{
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
builder.set_certificate_chain_file("certs.pem").unwrap();
// Require client certificates
builder.set_verify(openssl::ssl::SslVerifyMode::PEER | openssl::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT,
|_ssl, _cert, _err| true);
builder.build()
},
)?;
server.await
}
2. Client-side request with idempotency and nonce
use reqwest::Client;
use std::time::{SystemTime, UNIX_EPOCH};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::builder()
.use_rustls_tls()
.add_root_certificate(load_server_cert()?) // load trusted CA
.build()?;
let idempotency_key = format!("req-{}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos());
let payload = serde_json::json!({
"transfer_id": "xyz-123",
"amount": 100,
});
let response = client
.post("https://localhost:8443/transfer")
.header("Idempotency-Key", idempotency_key)
.json(&payload)
.cert(load_client_cert()?)?
.key(load_client_key()?)?
.send()
.await?;
println!("Status: {}", response.status());
Ok(())
}
fn load_server_cert() -> Result> {
let cert = std::fs::read("certs.pem")?;
Ok(reqwest::Certificate::from_pem(&cert)?)
}
fn load_client_cert() -> Result> {
let cert = std::fs::read("client.pem")?;
// parse PEM into rustls::Certificate
// simplified; use proper PEM parsing in production
unimplemented!()
}
fn load_client_key() -> Result> {
let key = std::fs::read("client.key")?;
// parse PEM into rustls::PrivateKey
unimplemented!()
}
3. Additional protections
- Include a timestamp or short-lived nonce in each request and verify server-side that it is recent (e.g., within 2 minutes) to limit replay windows.
- Use strong idempotency keys that are unique per business operation and store them with a TTL slightly longer than the expected client retry window.
- Combine mTLS with these application-level controls; do not rely on mTLS alone to prevent replay.
These fixes map to the middleBrick findings for Authentication, BOLA/IDOR, and Unsafe Consumption by ensuring requests are validated beyond transport identity and by providing actionable remediation guidance.