Man In The Middle in Django with Hmac Signatures
Man In The Middle in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A Man In The Middle (MitM) attack against a Django service that uses HMAC signatures can occur when the integrity protection is implemented incompletely or when transport security is absent. HMACs ensure request integrity by signing a canonical representation of the payload and keying material, but they do not inherently prevent interception or replay if critical controls are missing.
In a typical integration, a client computes an HMAC over the request body and selected headers (e.g., using a shared secret and a deterministic algorithm such as HMAC-SHA256) and sends the signature in a custom header. If the request is transmitted over unencrypted HTTP, an on-path attacker can observe the data and the signature. While the attacker cannot forge a valid signature without the secret, they can still perform a passive interception, harvesting sensitive information. More critically, if the server accepts requests without verifying transport layer security or does not enforce strict referential integrity checks, an attacker may attempt to replay captured signed requests or manipulate non-idempotent operations.
Django applications that consume signed webhook-like payloads must be cautious about where and how HMAC verification occurs. A common vulnerability pattern is to verify the HMAC only on the view that processes the data, while earlier middleware or load balancers terminate or alter the request in a way that invalidates the original transmission context. For example, if a request is forwarded through an HTTPS-terminated proxy without preserving the original headers and body exactly, the server-side recomputed HMAC may fail or be bypassed, creating an implicit trust boundary that an attacker can exploit.
Another specific risk arises when the HMAC is computed over a subset of the data that an attacker can influence indirectly. Consider a scenario where a timestamp or nonce is included in the signature, but the server does not enforce freshness checks or strict replay windows. An attacker can intercept a valid signed request and resend it within the acceptable window, causing duplicate operations or unauthorized state changes. This is especially dangerous for actions such as payments or configuration updates, where replay of a signed payload can have immediate business impact.
Additionally, weak handling of the shared secret compromises the entire scheme. If the secret is stored in environment variables that are inadvertently logged, exposed via debug pages, or transmitted over insecure channels during deployment, an attacker who performs MitM can leverage the intercepted secret to forge future requests. In Django, this often stems from improper configuration management rather than the cryptographic primitive itself, but it amplifies the consequences of a MitM scenario.
Real-world attack patterns include tampering with non-HMAC-covered metadata, exploiting inconsistent signature validation across endpoints, and leveraging timing differences in signature verification to infer information. Even when HTTPS is used, failing to validate host headers strictly can enable SSL-stripping or redirect-based tricks that position the attacker between client and server, making signed requests appear legitimate when they should be rejected.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
Defensive remediation centers on consistent, server-side verification, canonicalization of signed data, and strict transport requirements. Below are concrete, idiomatic Django patterns to implement HMAC verification safely.
Example 1: Basic HMAC-SHA256 verification for JSON payloads
import hmac
import hashlib
import json
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
@csrf_exempt
def webhook_handler(request):
if request.method != 'POST':
return HttpResponseBadRequest('Only POST allowed')
body = request.body # raw bytes
signature_header = request.META.get('HTTP_X_SIGNATURE')
if not signature_header:
return JsonResponse({'error': 'Missing signature'}, status=400)
secret = settings.WEBHOOK_SHARED_SECRET.encode('utf-8')
computed = hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(computed, signature_header):
return JsonResponse({'error': 'Invalid signature'}, status=401)
# Safe to process
try:
data = json.loads(body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
# Process data...
return JsonResponse({'status': 'ok'})
Example 2: Canonicalization with sorted keys and timestamp/nonce replay protection
import hmac
import hashlib
import json
import time
from django.http import JsonResponse
from django.conf import settings
REPLAY_WINDOW = 300 # seconds
seen_nonces = set() # In production, use a distributed cache with TTL
def compute_signature(secret: bytes, payload: dict) -> str:
# Canonical form: sorted keys, no extra whitespace
canonical = json.dumps(payload, separators=(',', ':'), sort_keys=True)
return hmac.new(secret, canonical.encode('utf-8'), hashlib.sha256).hexdigest()
def validate_replay(nonce: str, timestamp: float) -> bool:
if abs(time.time() - timestamp) > REPLAY_WINDOW:
return False
if nonce in seen_nonces:
return False
seen_nonces.add(nonce)
# Prune old nonces periodically in production
return True
@csrf_exempt
def secure_webhook(request):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
secret = settings.WEBHOOK_SHARED_SECRET.encode('utf-8')
received_sig = request.META.get('HTTP_X_SIGNATURE')
if not received_sig:
return JsonResponse({'error': 'Missing signature'}, status=400)
# Ensure required fields
if 'nonce' not in data or 'timestamp' not in data:
return JsonResponse({'error': 'Missing nonce or timestamp'}, status=400)
# Verify replay protection
if not validate_replay(data['nonce'], data['timestamp']):
return JsonResponse({'error': 'Replay or expired timestamp'}, status=401)
# Compute over canonical payload (excluding signature if present)
payload_for_sig = {k: v for k, v in data.items() if k != 'sig'}
expected_sig = compute_signature(secret, payload_for_sig)
if not hmac.compare_digest(expected_sig, received_sig):
return JsonResponse({'error': 'Invalid signature'}, status=401)
# Process business logic
return JsonResponse({'status': 'accepted'})
Remediation checklist and operational guidance
- Always use HTTPS to prevent passive interception; treat HMAC as integrity protection, not transport security.
- Use
hmac.compare_digestto avoid timing attacks on signature comparison. - Canonicalize the payload before signing (sorted keys, deterministic serialization) to avoid discrepancies between sender and receiver.
- Include and validate a nonce or timestamp with a bounded replay window to prevent reuse of captured requests.
- Store shared secrets securely (e.g., environment variables managed via secrets store) and rotate them periodically.
- Verify the signature before performing any side effects or state changes to avoid partial processing of malicious payloads.
- Log verification failures for monitoring, but avoid exposing sensitive data in logs.
In Django, you can encapsulate this logic into a reusable decorator or middleware to enforce consistent verification across endpoints, and integrate it with your CI/CD pipeline using the middleBrick CLI to scan for implementation issues automatically.