Ssrf Server Side in Django with Hmac Signatures
Ssrf Server Side in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Server-side request forgery (SSRF) in Django can be influenced by how you build and verify HMAC signatures. When a Django app uses HMAC signatures to authorize an incoming request, it typically computes a signature from selected parts of the request (e.g., selected headers, query parameters, or a JSON body field) and compares it to a value provided by the client. If the logic that selects what to sign is too permissive, or if the verification step does not tightly constrain the data used for signing, an attacker can trick the app into making arbitrary outbound HTTP requests.
Consider an endpoint that accepts a URL and an HMAC signature intended to prove the client controlled the URL. If the server recomputes the HMAC over only a subset of the request data (e.g., a single query parameter like url) and does not enforce strict schema or host validation before using the URL, an attacker can supply a malicious URL pointing to an internal service (e.g., http://localhost:8000/admin) or a metadata service (e.g., http://169.254.169.254). Because the HMAC verification passes, Django proceeds to fetch the URL, performing the SSRF on the server side. This pattern is common when APIs accept webhook callbacks or pre-signed URLs and rely on HMACs for integrity without also validating destination constraints.
Another scenario involves signed JSON payloads where the HMAC covers a selected subset of fields. If the server trusts fields that should be immutable (like an identifier used in signing) and does not validate or escape them before making a request, an attacker can embed a malicious host in a field that affects the outbound request. For example, a field intended for business logic (such as callback_url) might be included in the signature base string, but if the application later uses that field to drive an HTTP request without additional validation, the signed value can become the SSRF vector. The HMAC ensures integrity but does not prevent the application from misinterpreting a carefully crafted, signed input as a safe destination.
Django’s ecosystem can inadvertently encourage these patterns when developers use generic signature utilities without tying signature scope to a strict policy. For instance, computing an HMAC over raw query string parameters with hash_hmac style logic in Python (using hmac.compare_digest) may look safe, but if the code later appends additional parameters or uses the signed URL in a redirect or outbound fetch, the original verification no longer matches intent. Likewise, including dynamic elements like timestamps in the signature can reduce reproducibility for monitoring but also broaden the set of values that can be signed, increasing the chance that an SSRF-prone endpoint is reachable.
To summarize the risk: HMAC signatures in Django are not a substitute for strict input validation and destination control. An SSRF flaw arises when the application allows the client to influence which data is signed and then uses that data to make an outbound request without additional constraints. The signature provides integrity, but if the signed data includes or affects the target URL, port, or internal network path, an attacker can abuse the trusted signature to force the server into unwanted network interactions.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
Remediation centers on strict validation of the data that influences the request and scoping the HMAC to a clearly defined policy. Do not include mutable or attacker-controlled fields in the signature base unless those fields are also validated against a strict allowlist. Prefer signing a canonical representation of the intent (e.g., an operation ID and a timestamp) rather than the full URL or host.
Example 1: Signing a limited set of parameters and verifying before use.
import hmac import hashlib import time from django.http import JsonResponse from django.views.decorators.http import require_http_methods def generate_hmac(data: dict, secret: str) -> str: message = '|'.join(f'{k}={v}' for k, v in sorted(data.items())) return hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() @require_http_methods(['GET']) safe_redirect_view(request): timestamp = int(time.time()) operation = 'fetch_metadata' provided_sig = request.GET.get('sig') expected_sig = generate_hmac({'op': operation, 'ts': timestamp}, 'your-secret-key') if not hmac.compare_digest(expected_sig, provided_sig): return JsonResponse({'error': 'invalid signature'}, status=400) # Only now construct the request to a pre-approved internal endpoint internal_url = 'https://internal-api.example.com/v1/metadata' # additional validation of internal_url can happen here return JsonResponse({'url': internal_url, 'ts': timestamp})Example 2: Using a signed token that binds to a specific operation and destination.
import hmac import hashlib import json import base64 from urllib.parse import urlparse def sign_token(payload: dict, secret: str) -> str: payload_bytes = json.dumps(payload, separators=(',', ':')).encode() return base64.urlsafe_b64encode(hmac.new(secret.encode(), payload_bytes, hashlib.sha256).digest()).decode() def verify_and_extract_token(token: str, secret: str) -> dict: try: decoded = base64.urlsafe_b64decode(token + '==') except Exception: raise ValueError('invalid token') expected = sign_token(json.loads(decoded), secret) if not hmac.compare_digest(expected, token): raise ValueError('signature mismatch') return json.loads(decoded) # In a view from django.http import HttpResponseBadRequest def webhook_handler(request): token = request.POST.get('token') try: data = verify_and_extract_token(token, 'your-secret-key') except ValueError: return HttpResponseBadRequest('invalid token') # Ensure the operation and destination are within policy if data.get('op') != 'webhook_dispatch': return HttpResponseBadRequest('unsupported operation') # Validate destination against an allowlist allowed_hosts = {'api.example.com', 'internal.example.com'} parsed = urlparse(data['url']) if parsed.hostname not in allowed_hosts: return HttpResponseBadRequest('destination not allowed') # Proceed with controlled request return HttpResponse('ok')Key practices: (1) exclude the target URL from the signed payload or tightly constrain it to a pre-approved list; (2) validate the URL’s host, port, and scheme before any network call; (3) use
hmac.compare_digestto avoid timing attacks; (4) keep the signature scope minimal and tied to a server-side policy rather than echoing broad client input; (5) avoid including sensitive or mutable data in the signed string, and treat HMAC as integrity, not authorization.