Replay Attack in Django with Hmac Signatures
Replay Attack in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request containing an HMAC signature and re-transmits it to the server to perform an unauthorized action. In Django, using HMAC signatures for request authentication can protect integrity, but if nonces or timestamps are not enforced, the signed payload can be reused. Consider a Django view that signs a JSON payload with a shared secret using hmac.new. The signature is verified on the server, but if the request does not include a unique value per transaction, an attacker can simply forward the same request and the signature will still validate successfully.
For example, imagine a payment endpoint that accepts a signed JSON body with an amount and an account ID. The signing process might look like this:
import hmac
import hashlib
def generate_signature(secret, payload):
return hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
If this signature is sent without a nonce or timestamp, a captured request can be replayed indefinitely. Django does not inherently prevent replay unless you add a uniqueness constraint on each signed request. Common root causes include missing one-time tokens, lack of server-side caching for seen nonces, and not validating request freshness. Even when signatures confirm data integrity, they do not guarantee freshness, which is essential to prevent replay.
Additionally, if the secret used for HMAC is static and exposed (for instance, hard-coded or logged), an attacker who obtains the secret can forge signatures and replay requests at will. Another scenario involves APIs that accept query parameters or headers for signature verification without ensuring that each request includes a monotonically increasing number or a UUID. Without such protections, the API will treat repeated signed payloads as legitimate, making replay straightforward.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate replay in Django when using HMAC signatures, include a nonce and a timestamp in the signed payload, verify uniqueness server-side, and enforce a short validity window. Below is a concrete example that combines these practices.
First, define a helper to generate a signed payload with a timestamp and a random nonce:
import hmac
import hashlib
import time
import secrets
def generate_signed_payload(secret, data):
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
message = f'{timestamp}:{nonce}:{data}'
signature = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
return {'timestamp': timestamp, 'nonce': nonce, 'data': data, 'signature': signature}
On the server side, create a verification function that checks the timestamp window, ensures the nonce has not been used before, and validates the HMAC:
from django.core.cache import cache
import hmac
import hashlib
import time
def verify_hmac_signature(secret, received_timestamp, received_nonce, received_data, received_signature, window=300):
# Check timestamp freshness (e.g., 5 minutes)
now = int(time.time())
if abs(now - int(received_timestamp)) > window:
raise ValueError('Timestamp out of acceptable window')
# Ensure nonce uniqueness using cache (or database)
cache_key = f'hmac_nonce:{received_nonce}'
if cache.get(cache_key):
raise ValueError('Nonce already used')
cache.set(cache_key, True, timeout=window)
# Recompute signature
message = f'{received_timestamp}:{received_nonce}:{received_data}'
expected_signature = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, received_signature):
raise ValueError('Invalid signature')
return True
In your Django view, parse the incoming request, extract the timestamp, nonce, data, and signature, and call the verification function before processing business logic:
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt
def payment_endpoint(request):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
body = request.body.decode('utf-8')
payload = json.loads(body)
secret = 'your-shared-secret'
try:
verify_hmac_signature(
secret=secret,
received_timestamp=payload['timestamp'],
received_nonce=payload['nonce'],
received_data=payload['data'],
received_signature=payload['signature']
)
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
# Process payment safely knowing the request is fresh and authentic
return JsonResponse({'status': 'ok'})
By binding the signature to a timestamp and a unique nonce, and by checking nonces in a shared cache, you effectively prevent replay. The short validity window limits exposure, and cache-based deduplication ensures that each signed payload is accepted at most once within the window.