Vulnerable Components in Fastapi with Hmac Signatures
Vulnerable Components in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
HMAC-based authentication in FastAPI relies on a shared secret to sign requests and verify integrity. When implementation details deviate from best practices, the combination of FastAPI routing and HMAC verification introduces specific vulnerable components.
- Insecure signature comparison: Using standard string equality to compare the computed HMAC with the signature from the client enables timing attacks. An attacker can measure response times to iteratively guess the signature, especially when nonces or timestamps are involved. This is common when developers use
==instead of a constant-time comparison, effectively weakening the HMAC guarantee. - Missing or weak key management: Hardcoding the shared secret in source code or configuration files that are committed to version control is a prevalent issue. If the secret is exposed, an attacker can forge valid signatures for any payload. Additionally, using a low-entropy secret or reusing the same key across multiple services or environments amplifies the impact of a leak.
- Improper payload canonicalization: HMAC signs the exact byte sequence provided. If FastAPI deserializes JSON and re-serializes it differently before signing or verifying (e.g., due to field ordering, whitespace, or type conversions like numeric strings), the signature will not match. This can lead to logic bypasses where an attacker alters non-signature fields (such as user ID or permissions) while keeping a valid signature.
- Replay and lack of nonce/timestamp: HMAC signatures by themselves do not prevent replay. Without a nonce, timestamp, or short-lived token binding, an intercepted signed request can be resent to the endpoint to perform unauthorized actions, such as changing another user’s email or initiating a payment.
- Insufficient validation of all signed fields: If the signature covers only a subset of request parameters (for example, the body but not an API key or version header), an attacker can modify unsigned parts to escalate privileges or change behavior. In FastAPI, partial coverage commonly occurs when signing only the JSON payload while omitting headers that influence authorization logic.
- Endpoint inconsistency and method confusion: Using the same HMAC scheme for both public and privileged endpoints without scope separation can expose more surface. A publicly accessible endpoint that verifies HMAC without additional authentication may allow probing or enumeration if error messages differ between valid and invalid signatures.
These components interact with FastAPI’s dependency injection and request lifecycle. For instance, placing verification in a global dependency can simplify reuse but may inadvertently expose endpoints if the dependency is misconfigured. The framework does not inherently protect against implementation-level weaknesses in how the HMAC is computed, compared, or scoped.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
Secure HMAC usage in FastAPI centers on constant-time comparison, strong key management, canonical payload handling, and replay protection. Below are concrete, working examples that address the vulnerable components described above.
Secure HMAC verification with constant-time comparison
Use hmac.compare_digest to avoid timing attacks. Compute the HMAC over the exact bytes used for signing, and ensure the comparison is independent of attacker-controlled data length.
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException, Header, Depends
from typing import Optional
app = FastAPI()
# Load from environment in production; keep it out of source code.
# Example: import os; SECRET_KEY = os.environ["HMAC_SECRET_KEY"].encode()
SECRET_KEY = b"super-secret-key-32-bytes-long-examples-only"
def verify_hmac(body: bytes, received_sig: str) -> bool:
computed = hmac.new(SECRET_KEY, body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, received_sig)
@app.post("/webhook")
async def webhook_endpoint(
request: Request,
x_signature: Optional[str] = Header(None, alias="X-Signature")
):
if x_signature is None:
raise HTTPException(status_code=400, detail="Missing signature")
body = await request.body()
if not verify_hmac(body, x_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
# Process the verified payload here
return {"status": "ok"}
Canonical JSON payload handling
Ensure consistent serialization by specifying separators and sorting keys. This prevents signature mismatches due to formatting differences.
import json
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException, Header
app = FastAPI()
SECRET_KEY = b"example-key"
def canonical_json_serialize(data) -> bytes:
# sort_keys=True ensures deterministic field ordering
# separators=(',', ':') removes unnecessary whitespace
return json.dumps(data, sort_keys=True, separators=(',', ':')).encode()
# When you need to sign what you receive, parse, modify carefully, and re-sign:
@app.post("/order")
async def create_order(request: Request, x_signature: Optional[str] = Header(None)):
raw = await request.body()
if not hmac.compare_digest(
hmac.new(SECRET_KEY, raw, hashlib.sha256).hexdigest(),
x_signature
):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = json.loads(raw)
# Apply business logic, then if you forward or re-sign, use canonical_json_serialize
return {"received": True}
Replay protection with nonce and timestamp
Include a short-lived timestamp and a nonce in the signed payload to prevent replay attacks. Verify both signature and freshness on the server.
import time
import uuid
import hmac
import hashlib
import json
from fastapi import FastAPI, Request, HTTPException, Header
app = FastAPI()
SECRET_KEY = b"example-key"
def build_signed_payload(data, secret_key):
data["iat"] = int(time.time())
data["nonce"] = str(uuid.uuid4())
payload = json.dumps(data, sort_keys=True, separators=(',', ':')).encode()
sig = hmac.new(secret_key, payload, hashlib.sha256).hexdigest()
return payload, sig
# Verification example
@app.post("/action")
async def perform_action(request: Request, x_signature: Optional[str] = Header(None)):
raw = await request.body()
if not hmac.compare_digest(hmac.new(SECRET_KEY, raw, hashlib.sha256).hexdigest(), x_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = json.loads(raw)
iat = payload.get("iat")
if iat is None or (time.time() - iat) > 300: # 5 minutes window
raise HTTPException(status_code=400, detail="Request expired or missing timestamp")
# Process the action
return {"status": "processed"}
These patterns address the vulnerable components by eliminating timing leaks, ensuring deterministic signing inputs, protecting keys, and adding replay resistance. They integrate naturally into FastAPI’s dependency system and request handling without requiring internal architecture changes.