Api Rate Abuse in Fastapi with Hmac Signatures
Api Rate Abuse in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Rate abuse in FastAPI applications that rely on HMAC signatures for request authentication can occur when rate-limiting is applied after signature verification or is based only on attributes that do not prevent signature replay. HMAC signatures bind a request to a shared secret and a set of headers or a payload body, but they do not inherently provide replay protection. If the API validates the HMAC and then applies per-client or per-IP rate limits, an attacker with a valid signature can reuse it within the allowed rate window, leading to credential stuffing, brute-force attempts on business logic, or exhaustion of downstream resources.
A common pattern is to compute the HMAC over selected headers (e.g., X-API-Key, X-Timestamp, and the request body) and include the timestamp to enforce short validity. However, if the server does not enforce strict one-time use or nonce tracking for signed requests, an attacker can replay the same signed request multiple times before the timestamp window expires. Additionally, if rate limiting is applied globally or per API key without considering the signer identity, a single compromised key can generate many signed requests that each comply with rate limits but collectively abuse the endpoint.
For example, consider an endpoint that accepts POST /transfer with an HMAC signature in a header such as X-Signature. An attacker who captures a valid signature can resend the identical request at high frequency if the server only limits requests per IP or per API key. The server will verify the signature successfully on each attempt because the signature remains valid within the allowed time skew. This exposes the transfer operation to rapid exploitation, potentially bypassing protections that rely solely on request-level throttling.
The interaction between HMAC validation and rate limiting also affects observability and mitigation. Because the request is authenticated at the middleware or dependency level, generic rate-limiting middleware might not differentiate between a legitimately high volume of signed requests and an automated attack. Without coupling rate policy to the signer identity or including a nonce or request-id in the signed payload, the API lacks the means to detect and block replay-style abuse. Therefore, defenses must address both the integrity properties of HMAC and the operational characteristics of rate control to reduce the risk of abuse.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
To mitigate rate abuse when using HMAC signatures in FastAPI, you should combine per-request nonces or timestamps with replay tracking, bind rate limits to the signer identity, and validate anti-replay metadata before or during signature verification. Below are concrete code examples that demonstrate these practices.
1. Compute and verify HMAC with timestamp and nonce:
import time
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException, Depends
from typing import Optional
app = FastAPI()
# Shared secret should be stored securely, e.g., from environment or secrets manager
SHARED_SECRET = b"super-secret-key"
def compute_signature(timestamp: str, nonce: str, body: str) -> str:
message = f"{timestamp}:{nonce}:{body}".encode()
return hmac.new(SHARED_SECRET, message, hashlib.sha256).hexdigest()
def verify_hmac(request: Request, received_signature: str, timestamp: str, nonce: Optional[str] = None) -> bool:
# Reconstruct body for signature verification (be careful with streaming bodies)
body = request.body().decode()
expected = compute_signature(timestamp, nonce or "", body)
return hmac.compare_digest(expected, received_signature)
2. Enforce replay protection with a short nonce cache and rate limiting per signer:
from fastapi import FastAPI, Request, HTTPException, Depends
import aioredis
import time
import hmac, hashlib
app = FastAPI()
redis = aioredis.from_url("redis://localhost")
async def is_replayed(nonce: str, window: int = 60) -> bool:
# Store nonce with TTL slightly larger than allowed clock skew + validity window
key = f"nonce:{nonce}"
if await redis.exists(key):
return True
await redis.setex(key, window + 10, "used")
return False
async def rate_limited_for_signer(api_key: str, limit: int = 10, window: int = 60):
# Use a key that includes the signer to scope rate limits
key = f"rate:{api_key}"
current = await redis.incr(key)
if current == 1:
await redis.expire(key, window)
return current > limit
3. Example endpoint combining verification, replay check, and per-signer rate limiting:
@app.post("/transfer")
async def transfer(request: Request, x_api_key: str, x_timestamp: str, x_nonce: str, x_signature: str):
# Basic freshness check to prevent replay across long windows
now = str(int(time.time()))
if abs(int(now) - int(x_timestamp)) > 30:
raise HTTPException(status_code=400, detail="Timestamp out of allowed skew")
# Replay protection: reject previously seen nonce
if await is_replayed(x_nonce, window=60):
raise HTTPException(status_code=403, detail="Replay detected")
# Verify HMAC over timestamp:nonce:body
if not verify_hmac(request, x_signature, x_timestamp, x_nonce):
raise HTTPException(status_code=401, detail="Invalid signature")
# Per-signer rate limit
if await rate_limited_for_signer(x_api_key, limit=10, window=60):
raise HTTPException(status_code=429, detail="Rate limit exceeded for signer")
# Proceed with business logic
return {"status": "ok"}
These patterns couple rate control to the signer identity (API key or client ID), enforce timestamp validity to bound replay windows, and use nonces or replay caches to prevent reuse of signed requests. They address the interaction between HMAC authentication and rate limiting by ensuring that each signed request is unique and that abusive repetition is detected and throttled.