Man In The Middle in Fastapi with Hmac Signatures
Man In The Middle in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A Man In The Middle (MitM) attack against a FastAPI service that uses HMAC signatures can occur when transport protection is assumed but not enforced, or when signature verification is implemented inconsistently. HMAC provides integrity and authenticity, but it does not inherently prevent interception or replay if TLS is absent or misconfigured.
Consider a FastAPI endpoint that accepts a JSON payload and an X-API-Signature header. If the server validates the HMAC but does not require HTTPS, an attacker on the network can observe the request, capture the body and headers, and forward them to the server. Because the signature is valid (it was generated with the shared secret), the server processes the request as authorized. The attacker can also resend the captured request (replay) while the signature is still valid, potentially changing nonces or timestamps if the server does not enforce strict replay protection.
Another scenario involves partial TLS coverage. If only some routes use HTTPS and others do not, clients might inadvertently send signed requests over plain HTTP. For example, a client might first fetch a public configuration over HTTP and then use a token from that response to call a signed endpoint. An attacker can intercept the unencrypted request, inject or modify parameters, and relay them to the HTTPS endpoint, relying on any lax origin checks or missing canonicalization in the signing process.
Signature misuse can also create vulnerabilities. If the server uses a predictable or leaked shared secret, the HMAC is compromised. Similarly, if the signing string is constructed ambiguously (e.g., different ordering of query parameters, inconsistent handling of whitespace, or failure to exclude sensitive headers), an attacker can craft colliding or functionally equivalent messages that bypass verification. In some implementations, failing to validate the X-API-Key alongside the HMAC may allow an attacker to rotate keys or substitute identifiers if the server trusts path parameters or host headers.
To illustrate, consider a FastAPI route that verifies an HMAC but does not enforce TLS and uses a simplistic signing approach:
import hashlib
import hmac
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
SHARED_SECRET = b"insecure-default-secret"
def verify_signature(body: str, signature: str) -> bool:
computed = hmac.new(SHARED_SECRET, body.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)
@app.post("/action")
async def perform_action(payload: dict, x_api_signature: str = Header(None)):
if x_api_signature is None:
raise HTTPException(status_code=400, detail="Missing signature")
body = ''.join(f"{k}{v}" for k, v in sorted(payload.items()))
if not verify_signature(body, x_api_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
return {"status": "ok"}
In this example, an attacker on the same network can intercept the plaintext HTTP request, copy the body and the X-API-Signature, and replay it to the server. The server will accept it because the signature matches. Additionally, if the client sometimes sends requests over HTTP due to misconfigured redirects or mixed content, the signature and payload are exposed in transit, enabling MitM modification before the request reaches a secure endpoint.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on enforcing transport security, canonicalizing the signed payload, and validating context alongside the HMAC. Always use HTTPS for any request that carries an HMAC signature, and reject requests that arrive over plain HTTP.
Use a deterministic method to construct the string that is signed. Include all relevant parts of the request—HTTP method, path, selected headers, and a canonical body—so that tampering with any component invalidates the signature. Avoid simple concatenation of sorted keys; instead, follow a structured approach such as joining key-value pairs with a newline and escaping values consistently.
Validate the presence and correctness of the signature header on every request, and consider binding the signature to additional metadata such as a timestamp or nonce to prevent replay attacks. Below is a robust FastAPI example that uses HTTPS enforcement, structured signing, timestamp validation, and HMAC comparison:
import hashlib
import hmac
import time
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
SHARED_SECRET = b"secure-random-high-entropy-secret"
MAX_CLOCK_SKEW = 30 # seconds
def build_string_for_signature(request: Request, body: str) -> str:
timestamp = request.headers.get("X-Request-Timestamp")
nonce = request.headers.get("X-Nonce")
if not timestamp or not nonce:
raise ValueError("Missing timestamp or nonce")
method = request.method
path = request.url.path
return f"{method}\n{path}\n{timestamp}\n{nonce}\n{body}"
def verify_signature(body: str, signature: str, timestamp: str, nonce: str) -> bool:
string_to_sign = f"{request.method}\n{request.url.path}\n{timestamp}\n{nonce}\n{body}"
computed = hmac.new(SHARED_SECRET, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)
@app.middleware("http")
async def enforce_https(request: Request, call_next):
if request.url.scheme != "https":
return JSONResponse({"error": "HTTPS required"}, status_code=403)
response = await call_next(request)
return response
@app.post("/action")
async def perform_action(request: Request, payload: dict, x_api_signature: str = Header(None), x_request_timestamp: str = Header(None), x_nonce: str = Header(None)):
if x_api_signature is None or x_request_timestamp is None or x_nonce is None:
raise HTTPException(status_code=400, detail="Missing required headers or body")
# Reject old requests to mitigate replay
try:
request_time = int(x_request_timestamp)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid timestamp")
if abs(time.time() - request_time) > MAX_CLOCK_SKEW:
raise HTTPException(status_code=401, detail="Request expired")
body = "".join(f"{k}{v}" for k, v in sorted(payload.items()))
string_to_sign = build_string_for_signature(request, body)
computed = hmac.new(SHARED_SECRET, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed, x_api_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
return {"status": "ok"}
This approach ensures that signatures are tied to the exact request context, reducing the risk of successful MitM or replay. Enforcing HTTPS prevents network-level interception, while timestamp and nonce checks bound the validity window. Consistent canonicalization of the signed string prevents ambiguity that attackers could exploit to create valid but malicious variations.
For production, rotate the shared secret periodically, store it securely, and avoid logging raw signatures or secrets. Combine HMAC validation with other transport protections and monitor for anomalous request patterns to further reduce the impact of any residual MitM exposure.