HIGH man in the middlefastapihmac signatures

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.

Frequently Asked Questions

Does using HMAC signatures alone prevent Man In The Middle attacks in FastAPI?
No. HMAC signatures provide integrity and authenticity but do not prevent interception. Without HTTPS, an attacker can capture and replay valid requests. Always enforce TLS and use canonical signing that includes method, path, timestamp, and nonce.
What are common implementation mistakes when using HMAC signatures in FastAPI?
Common mistakes include inconsistent ordering of signed fields, missing HTTPS enforcement, not validating timestamps or nonces (enabling replay), using a static or leaked shared secret, and failing to validate the signature header on every request. These can allow MitM or replay even when HMAC is present.