Auth Bypass in Django with Hmac Signatures
Auth Bypass in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
An authentication bypass can occur in Django when HMAC-based signatures are used to validate the integrity of a request or token but the implementation does not enforce strict verification, allow weak or predictable secrets, or fails to protect the comparison against timing attacks. HMAC is designed to ensure that a message has not been altered, but it does not inherently provide authentication of the sender if the verifying side does not properly associate the signature with a trusted key and origin.
In a Django API or web flow, a common pattern is to sign a payload (e.g., a JSON body or query parameters) on the client with a shared secret using HMAC-SHA256 and include the signature in a header such as X-API-Signature. The server then recomputes the HMAC over the received payload using the same secret and compares the two signatures. If the comparison is done with a naive string equality check, an attacker can exploit timing discrepancies to recover the expected signature byte by byte, leading to signature forgery and authentication bypass. This is an implementation weakness, not a flaw in HMAC itself.
Another bypass scenario arises when the shared secret is weak, hardcoded, or leaked, or when the signed data does not include sufficient contextual bindings such as timestamp, nonce, or scope. For example, if the signature covers only the request body but not the HTTP method or path, an attacker could replay a valid signed POST to an idempotent GET endpoint if the server does not validate the method or route as part of the signed context. Similarly, if the signature is computed over a subset of parameters and the server accepts additional unchecked parameters, an attacker may modify critical fields (e.g., changing role=user to role=admin) while preserving a valid signature, leading to privilege escalation or unauthorized actions.
Django-specific risks can also emerge from improper key management or from using settings that are accidentally exposed (e.g., via debug pages or logs). If the HMAC secret is derived from an insecure source or stored in client-side code, an attacker can obtain the key and generate valid signatures independently. Additionally, if the verification logic is implemented in middleware or a decorator without ensuring it runs for all relevant views, some endpoints may remain unverified, creating an implicit bypass through uncovered routes.
Consider a minimal example where a client signs a JSON body and the server verifies it. If the server trusts the Content-Type header but does not enforce strict parsing or canonicalization, an attacker might supply alternative representations of the same logical data (different key ordering, whitespace, or number formatting) that produce different hashes, allowing a modified payload to appear valid under a weak verification scheme. This is a canonicalization issue that can bypass the intended integrity protection even when HMAC-SHA256 is used correctly.
To summarize, the combination of Django, HMAC signatures, and insecure implementation choices—such as weak comparison, missing context binding, poor secret handling, or incomplete coverage—can create authentication bypass paths. The scanner categories relevant here include Authentication, Input Validation, and Unsafe Consumption, because the issue lies in how inputs, signatures, and credentials are handled rather than in the cryptographic primitive alone.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
Remediation focuses on using a constant-time comparison, canonicalizing inputs, binding contextual metadata, and protecting the HMAC secret. Below are concrete, realistic code examples that demonstrate a secure pattern for HMAC-signed requests in Django.
1. Use HMAC-SHA256 with a strong shared secret and constant-time comparison:
import hmac
import hashlib
from django.conf import settings
from django.http import JsonResponse, HttpResponseBadRequest
from django.views import View
SECRET_KEY = getattr(settings, 'HMAC_SHARED_SECRET', None)
if not SECRET_KEY:
raise ValueError('HMAC_SHARED_SECRET must be configured')
def verify_signature(body: bytes, received_sig: str) -> bool:
expected = hmac.new(SECRET_KEY.encode('utf-8'), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_sig)
class SignedPostView(View):
def post(self, request):
body = request.body # raw bytes
received_sig = request.META.get('HTTP_X_API_SIGNATURE')
if not received_sig:
return JsonResponse({'error': 'missing signature'}, status=400)
if not verify_signature(body, received_sig):
return JsonResponse({'error': 'invalid signature'}, status=403)
# process request safely
return JsonResponse({'status': 'ok'})
This ensures the secret is loaded from settings, the signature is computed over the raw body bytes, and comparison is done with hmac.compare_digest to prevent timing attacks.
2. Include contextual bindings to prevent replay and scope confusion:
import json
import time
import hmac
import hashlib
def build_signed_payload(data: dict, secret: str) -> tuple[bytes, str]:
payload = {
'iat': int(time.time()), # timestamp
'exp': int(time.time()) + 300, # 5-minute expiry
'data': data
}
body = json.dumps(payload, separators=(',', ':'), sort_keys=True).encode('utf-8')
sig = hmac.new(secret.encode('utf-8'), body, hashlib.sha256).hexdigest()
return body, sig
def verify_signed_payload(body: bytes, received_sig: str, secret: str) -> dict | None:
if not hmac.compare_digest(hmac.new(secret.encode('utf-8'), body, hashlib.sha256).hexdigest(), received_sig):
return None
try:
payload = json.loads(body)
except json.JSONDecodeError:
return None
now = int(time.time())
if now > payload.get('exp', 0):
return None
return payload.get('data')
By including iat (issued at) and exp (expiry) and canonicalizing JSON with sorted keys and compact separators, you reduce replay and ambiguity risks. The verification function validates the signature, checks timestamp bounds, and returns the inner data only when all checks pass.
3. Apply this verification in middleware or a reusable decorator to ensure coverage across views:
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
class HmacAuthMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.method not in ('GET', 'HEAD', 'OPTIONS'):
body = request.body
received_sig = request.META.get('HTTP_X_API_SIGNATURE')
if not verify_signature(body, received_sig):
raise SuspiciousOperation('Invalid HMAC signature')
# proceed to view
With these practices—strong secret management, canonicalized inputs, contextual metadata, constant-time comparison, and broad coverage—the risk of authentication bypass via HMAC signature weaknesses is substantially mitigated. The scanner categories affected include Authentication, Input Validation, and Unsafe Consumption, because the fix addresses how signatures, payloads, and credentials are handled.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |