Replay Attack in Flask with Hmac Signatures
Replay Attack in Flask 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 intended effect without going through the authentication logic. In Flask applications that rely on HMAC signatures to verify request integrity, a common pitfall is signing only a subset of the request components (for example, the payload) while omitting nonces or timestamps. Because HMAC is a deterministic algorithm, the same input always produces the same signature. If the server verifies the signature but does not enforce uniqueness per request, an attacker can capture a signed request—such as a payment or state-changing API call—and replay it multiple times with identical effect.
Consider a Flask route that expects a JSON body and an HMAC-SHA256 signature in a header, where the signature is computed over the raw request body. If the server only validates the signature and does not require a nonce or timestamp, an intercepted request can be replayed indefinitely. This is especially dangerous for idempotency-unsafe operations or when the application mistakenly assumes that transport-layer protections (such as TLS) prevent replay. Even with TLS, a malicious insider or a compromised proxy can capture and re-send traffic. The vulnerability is not in HMAC itself, which provides strong integrity guarantees, but in the application design that fails to bind the signature to a unique, single-use context.
Another contributing factor is poor handling of clock drift and replay windows. If a server accepts a timestamp that is too old or does not enforce a tight validity window, an attacker can slightly adjust timing to bypass freshness checks. In Flask, this can happen when developers implement custom timestamp checks without a strict window or without combining the timestamp with a nonce. The combination of HMAC signatures with missing replay protections—nonces, timestamps, or both—creates a scenario where authenticated endpoints remain vulnerable to replay, despite the presence of cryptographic integrity checks.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
To defend against replay attacks while using HMAC signatures in Flask, you must ensure each request includes a unique value (a nonce or a timestamp with sufficient precision) and that the server tracks or validates this uniqueness within an acceptable window. Below is a concrete, secure example that combines HMAC-SHA256 with a timestamp and a server-side cache of recently seen nonces to prevent replays.
import time
import hmac
import hashlib
import json
from flask import Flask, request, abort, g
import functools
app = Flask(__name__)
# Shared secret stored securely (e.g., from environment)
SECRET = b'super-secret-key-change-in-production'
# In production, replace this with a distributed cache (e.g., Redis) if you run multiple workers
seen_nonces = set()
NONCE_TTL_SECONDS = 300 # 5 minutes
def verify_hmac(request_body, received_signature):
computed = hmac.new(SECRET, request_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, received_signature)
@app.before_request
def enforce_hmac_and_replay_protection():
if request.method in ('POST', 'PUT', 'PATCH'):
signature = request.headers.get('X-API-Signature')
timestamp = request.headers.get('X-Request-Timestamp')
nonce = request.headers.get('X-Request-Nonce')
if not signature or not timestamp or not nonce:
abort(400, 'Missing signature, timestamp, or nonce')
# Enforce timestamp freshness (e.g., 5 minutes)
try:
ts = float(timestamp)
except ValueError:
abort(400, 'Invalid timestamp')
now = time.time()
if abs(now - ts) > 300:
abort(400, 'Request timestamp outside allowed window')
# Replay protection: reject previously seen nonces
if nonce in seen_nonces:
abort(400, 'Replay detected: nonce already used')
# Store nonce with a cleanup strategy (simplified)
seen_nonces.add(nonce)
g.nonce = nonce
g.timestamp = ts
# Verify HMAC over the raw body
if not verify_hmac(request.get_data(), signature):
abort(401, 'Invalid signature')
@app.route('/api/transfer', methods=['POST'])
def transfer():
# Business logic here; request is guaranteed fresh and authenticated
data = request.get_json()
return {'status': 'ok', 'received': data}, 200
if __name__ == '__main__':
app.run(ssl_context='adhoc')
Key points in the remediation:
- Include both a timestamp and a nonce in the signed scope. The timestamp ensures freshness, while the nonce guarantees uniqueness even if timestamps collide.
- Compute the HMAC over the raw request body (or a canonical representation of the important fields) and compare using
hmac.compare_digestto avoid timing attacks. - Enforce a tight timestamp window (e.g., 300 seconds) and reject requests outside this window to limit the replay window.
- Maintain a short-lived store of recently seen nonces to detect replays. In production, use a shared cache with TTL to handle multiple workers and restarts.
- Ensure the secret key is stored securely (environment variables or a secrets manager) and rotated periodically.
Alternative approach for frameworks that support request signing via a standard like Digest or framework-specific helpers is possible, but the principle remains: bind the signature to a unique, single-use context and validate freshness on every request.