Replay Attack in Fastapi with Hmac Signatures
Replay Attack in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and retransmits it to reproduce the original effect. In Fastapi applications that use Hmac signatures for request authentication, the vulnerability arises when the server validates the signature and timestamp but does not enforce strict one-time use or freshness guarantees. Hmac signatures bind the request payload and selected headers to a shared secret, ensuring integrity and origin authenticity. However, if the server only checks that the signature is valid and that the timestamp is within an acceptable window, the same signed payload can be replayed within that window without triggering any validation failure.
Consider a Fastapi endpoint that accepts a JSON body and an X-API-Timestamp header, verifying the Hmac signature over the concatenation of timestamp and body. An attacker can capture a legitimate signed request—such as a payment initiation or a resource creation—and resend it exactly as received. Because the signature remains valid and the timestamp may still fall within the allowed skew, the server processes the operation again, potentially causing duplicate transactions or unauthorized state changes. This risk is especially pronounced when idempotency is not enforced at the application level.
The attack surface is expanded when requests include query parameters or headers that are not part of the signed string, or when the timestamp resolution is coarse (for example, seconds rather than milliseconds), making it easier to reuse a valid signature across retries. Without additional protections such as nonce tracking or strict request deduplication, Fastapi’s Hmac-based scheme can inadvertently facilitate replay despite correct cryptographic verification.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
To mitigate replay attacks in Fastapi when using Hmac signatures, you must ensure each signed request is unique and time-bound. This involves incorporating a nonce and a precise timestamp into the signed string, enforcing monotonicity or single-use nonces, and rejecting requests with timestamps outside a tight window. Below are concrete code examples demonstrating these protections.
First, define a utility to compute the Hmac over a canonical string that includes a client-supplied nonce and timestamp:
import hmac
import hashlib
import time
import secrets
SHARED_SECRET = b'your-256-bit-secret'
def compute_hmac(nonce: str, timestamp: str, payload: str) -> str:
message = f'{nonce}|{timestamp}|{payload}'.encode('utf-8')
return hmac.new(SHARED_SECRET, message, hashlib.sha256).hexdigest()
On the server side, maintain a short-lived cache of recently seen nonces to prevent reuse. Use a fixed time window (for example, 30 seconds) and enforce that the request timestamp is within that window:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import time
app = FastAPI()
# Simple in-memory store; consider a distributed cache in production
seen_nonces = set()
TIMESTAMP_TOLERANCE = 30 # seconds
def verify_hmac(request: Request, payload: str) -> bool:
timestamp = request.headers.get('X-API-Timestamp')
nonce = request.headers.get('X-API-Nonce')
signature = request.headers.get('X-API-Signature')
if not all([timestamp, nonce, signature]):
return False
# Check timestamp freshness
now = str(int(time.time()))
if abs(int(now) - int(timestamp)) > TIMESTAMP_TOLERANCE:
return False
# Reject replayed nonces
if nonce in seen_nonces:
return False
expected = compute_hmac(nonce, timestamp, payload)
if not hmac.compare_digest(expected, signature):
return False
# Record nonce for the duration of the tolerance window
seen_nonces.add(nonce)
return True
@app.post('/process')
async def process_item(request: Request):
body = await request.body()
body_str = body.decode('utf-8')
if not verify_hmac(request, body_str):
raise HTTPException(status_code=401, detail='Invalid or replayed request')
# Safe to process
return JSONResponse({'status': 'ok'})
For production use, replace the in-memory set with a TTL-aware store to avoid memory growth and to support multiple workers. Additionally, include HTTP method and path in the signed string if the endpoint semantics depend on the verb or route. This approach ensures that even if an attacker captures a valid Hmac-signed request, it cannot be reused outside the narrow time window and nonce constraints.