Cross Site Request Forgery in Django with Hmac Signatures
Cross Site Request Forgery in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) in Django typically relies on the framework’s built-in protections: the CSRF token included in forms and checked on state-changing POST requests. When developers introduce Hmac Signatures as an additional or alternative integrity mechanism, they can inadvertently weaken or bypass Django’s CSRF protections if the signature is used in place of the CSRF token rather than alongside it.
Consider a design where a client computes an Hmac over a request payload and sends it in a custom header (e.g., X-API-Signature) but does not include Django’s CSRF token in the form or header. Because Django’s CSRF middleware only validates the CSRF token by default, requests with a valid Hmac but no CSRF token will pass the signature check yet still be vulnerable to CSRF if an attacker can trick a victim’s browser into making a request with that Hmac. The Hmac may verify the integrity of the payload, but it does not bind the request to a user’s session in the way Django’s CSRF protection does, since the signature is often computed without a per-session secret or without tying it to the session cookie.
Another common pattern is using Hmac signatures for API-style endpoints while disabling CSRF protection via @csrf_exempt. If the endpoint does not implement an alternative mechanism that proves the request originates from a legitimate client (e.g., by validating a signature derived from the user’s session or a per-request nonce), the endpoint becomes effectively CSRF-exposed for any authenticated user. For example, an attacker could craft a request with a valid Hmac if the key is leaked or predictable, or could induce a logged-in user to perform unintended actions if the signature does not incorporate a per-request token tied to the session.
Real-world cases include endpoints that accept JSON payloads with an Hmac in a header but do not validate the Referer or Origin headers, and do not require the CSRF token. Since Django’s CSRF middleware is bypassed by exempt views, and the Hmac alone does not provide anti-replay or per-session binding, the attack surface mirrors classic CSRF: an authenticated attacker can forge requests on behalf of a victim. The presence of Hmac Signatures does not automatically prevent CSRF; without careful design—such as binding the signature to the session and ensuring the view still respects Django’s CSRF protections—the combination can create a false sense of security.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To securely use Hmac Signatures in Django while preserving CSRF protection, treat the signature as an additional integrity check, not a replacement for Django’s CSRF mechanism. Always require Django’s CSRF token for state-changing requests, and ensure the Hmac is computed over a scope that includes the session or a per-request nonce to prevent replay and CSRF.
Example: a view that validates both CSRF and an Hmac signature derived from the request body and a server-side secret combined with the user’s session key. This ensures that even if an attacker can forge a request, they cannot produce a valid Hmac without the session-specific secret.
import hmac
import hashlib
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_protect
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views import View
SECRET_KEY = b'your-server-side-secret'
def compute_signature(data: dict, session_key: str) -> str:
payload = json.dumps(data, sort_keys=True, separators=(',', ':'))
message = session_key.encode('utf-8') + b'|' + payload.encode('utf-8')
return hmac.new(SECRET_KEY, message, hashlib.sha256).hexdigest()
@method_decorator([csrf_protect, login_required], name='dispatch')
class SecureHmacView(View):
def post(self, request):
# Ensure CSRF cookie is present and validated by Django middleware
# Compute expected signature using session-specific material
session_key = request.session.session_key or ''
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'invalid json'}, status=400)
expected = compute_signature(body, session_key)
provided = request.META.get('HTTP_X_API_SIGNATURE', '')
if not hmac.compare_digest(expected, provided):
return JsonResponse({'error': 'invalid signature'}, status=403)
# Process the request knowing both CSRF and Hmac are validated
return JsonResponse({'status': 'ok'})
If you use the CLI tool, you can scan endpoints that use custom headers to verify they do not rely solely on Hmac without CSRF: middlebrick scan https://api.example.com/order. The Dashboard helps track scans over time, and the Pro plan’s continuous monitoring can alert you if a new endpoint lacks CSRF protection despite using Hmac Signatures.
For API clients that cannot rely on cookies (e.g., mobile apps), bind the Hmac to a per-request token stored in a secure, httpOnly cookie or passed in an Authorization header, and validate both the token and the signature on the server. Never exempt CSRF protection (@csrf_exempt) for endpoints that accept state-changing actions unless you have an alternative mechanism that proves request authenticity, such as a one-time nonce tied to the user’s session.
The GitHub Action can enforce that new endpoints include CSRF validation checks in code reviews, failing the build if a view uses csrf_exempt without compensating controls. This keeps CSRF and Hmac Signatures aligned so that security is not dependent on a single control.