Man In The Middle in Actix with Hmac Signatures
Man In The Middle in Actix with Hmac Signatures — how this specific combination creates or exposes the vulnerability
In an Actix-based service, using Hmac Signatures without transport integrity controls can enable a Man In The Middle (MitM) scenario where an attacker intercepts and potentially alters requests between client and server. Hmac Signatures bind a request payload to a shared secret, but they do not inherently prevent interception; they only verify integrity and origin if the secret is safe and the signature covers the full canonical representation of the message. If you validate signatures without enforcing authenticated encryption on the transport, an attacker who can observe and modify traffic might change non-signature parts of the request (such as method, path, or selected headers) and may cause the server to act on tampered intent, despite a valid Hmac check on the payload body.
A common pattern in Actix involves reading the raw body, extracting signature headers, and verifying using a shared secret. If the server does not also enforce HTTPS and does not include the request target (path and query) and critical headers in the signed string, an attacker can switch a POST to a different endpoint or change an idempotent flag. This becomes especially risky when the signature algorithm or key management is weak, or when the server accepts multiple signature formats without strict canonicalization, allowing an attacker to craft a valid-looking signature under a weaker algorithm or key derivation. Such weaknesses can lead to privilege escalation or unauthorized actions even though each message carries a Hmac Signature.
Consider an endpoint that transfers funds identified by an integer ID. An attacker performing MitM might change the ID in the URL while keeping the body and Hmac Signature intact if the signature does not cover the path parameter. In Actix, if route parameters are not included in the signed string, the server may trust the modified ID and process the request against a different account. Additionally, without replay protection or nonce verification, an intercepted signed request can be replayed later to repeat sensitive operations. This illustrates why Hmac Signatures must be combined with channel binding to HTTPS and a carefully designed signed scope that includes method, path, and selected headers to reduce the impact of a Man In The Middle in Actix deployments.
Hmac Signatures-Specific Remediation in Actix — concrete code fixes
To mitigate Man In The Middle risks when using Hmac Signatures in Actix, ensure the transport is authenticated and the signed scope covers all components an attacker could modify. Always serve endpoints over TLS to prevent on-path alteration of method, path, and headers. Include the HTTP method, the full path (with canonical query ordering), and any headers that influence processing in the string that is signed. Use a strong key derivation mechanism and a stable canonicalization to avoid discrepancies between client and server.
The following example demonstrates a secure approach in Actix using middleware to compute and verify Hmac Signatures. It covers method, path, and selected headers, and uses constant-time comparison to avoid timing attacks:
use actix_web::{dev::ServiceRequest, Error, HttpMessage};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::BTreeMap;
// alias for Hmac-SHA256
type HmacSha256 = Hmac;
/// Build a canonical string for signing: method + path + sorted query + selected headers
fn canonical_string(req: &ServiceRequest, header_keys: &[&str]) -> String {
let method = req.method().as_str();
let path = req.path();
let query = req.query_string();
// sort query parameters for canonical form
let mut params: Vec<_> = form_urlencoded::parse(query.as_bytes()).collect();
params.sort_by_key(|(k, _)| *k);
let canonical_query: String = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::>()
.join("&");
let query_part = if canonical_query.is_empty() { String::new() } else { format("?{}", canonical_query) };
let mut header_part = String::new();
for &key in header_keys {
if let Some(v) = req.headers().get(key) {
header_part.push_str(&format!("\n{}:{}", key, v.to_str().unwrap_or("")));
}
}
format!("{}:{}{}", method, path, query_part) + &header_part
}
/// Middleware to verify Hmac signature on incoming requests
async fn verify_hmac(req: ServiceRequest, secret: &[u8]) -> Result {
// header name expected from client, e.g., X-API-Signature
const SIGNATURE_HEADER: &str = "x-api-signature";
const HEADER_KEYS: &[&str] = &["content-type", "x-request-id"];
let signature = match req.headers().get(SIGNATURE_HEADER) {
Some(v) => v.to_str().map_err(|_| actix_web::error::ErrorBadRequest("Invalid signature header"))?,
None => return Err(actix_web::error::ErrorBadRequest("Missing signature")),
};
let body = match req.payload().poll_next_unpin(std::task::Context::from_waker(std::task::noop_waker_ref())) {
std::task::Poll::Ready(Some(Ok(chunk))) => chunk,
_ => return Err(actix_web::error::ErrorBadRequest("Unable to read body")),
};
let payload = std::str::from_utf8(&body).map_err(|_| actix_web::error::ErrorBadRequest("Invalid UTF-8 body"))?;
let mut mac = HmacSha256::new_from_slice(secret).map_err(|_| actix_web::error::ErrorBadRequest("Invalid key"))?;
mac.update(canonical_string(&req, HEADER_KEYS).as_bytes());
mac.update(payload.as_bytes());
let computed = mac.finalize();
let code = computed.into_bytes();
// constant-time comparison to avoid timing leaks
let Ok(signature_bytes) = hex::decode(signature) else {
return Err(actix_web::error::ErrorBadRequest("Invalid signature encoding"));
};
if subtle::ConstantTimeEq::ct_eq(&code[..], &signature_bytes[..]).into() {
Ok(req)
} else {
Err(actix_web::error::ErrorUnauthorized("Invalid Hmac signature"))
}
}
// Example route using the verification
async fn transfer(req: actix_web::HttpRequest, body: String) -> String {
format!("Processed: {}", body)
}
/// Factory to wrap your service with Hmac verification
fn with_hmac(service: F, secret: &'static [u8]) -> impl Fn(ServiceRequest) -> _ {
move |req| {
let secret = secret.to_vec();
async move {
verify_hmac(req, &secret).and_then(|req| {
let (req, pl) = req.into_parts();
actix_web::dev::ServiceResponse::new(req, pl)
})
}
}
}
Key points in this remediation:
- TLS is mandatory; Hmac does not encrypt the payload, so transport confidentiality is required to prevent MitM alteration of non-signature parts.
- The canonical string includes method, path, sorted query parameters, and selected headers to prevent tampering with route parameters or content-type confusion.
- Use a stable, deterministic canonicalization (sorted query keys) so client and server produce the same string.
- Perform constant-time comparison to avoid timing side-channels that could leak signature validity.
- Include a nonce or timestamp and enforce short validity windows to prevent replay attacks, which are common in MitM contexts.
For broader protection across your API surface, you can use the middleBrick CLI to scan from terminal with middlebrick scan <url> and review Hmac-related findings in the report. If you need continuous monitoring or CI/CD enforcement, the Pro plan provides GitHub Action integration to fail builds when risk scores drop below your threshold, helping catch insecure signing practices before deployment.