HIGH replay attackdjangohmac signatures

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.

Frequently Asked Questions

Why does HMAC alone not prevent replay attacks in Django?
HMAC ensures integrity and authenticity of the payload, but it does not guarantee freshness. Without a nonce or timestamp, a valid signature can be reused indefinitely, allowing an attacker to replay the same request.
How long should the timestamp validity window be for HMAC replay protection in Django?
A window of 300 seconds (5 minutes) is common, but you should balance security and usability. Shorter windows reduce replay risk but may cause legitimate requests to fail if there is clock skew or latency.