HIGH ssrf server sidedjangohmac signatures

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_digest to 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.

Frequently Asked Questions

Does including the URL in the HMAC signature prevent SSRF?
No. Including the URL in the signature ensures integrity, but if the application still uses that URL to make an outbound request without strict host and scheme validation, SSRF can still occur. Signature scope must be limited and combined with destination allowlists.
Is HMAC enough to secure webhook endpoints against tampering?
HMAC provides integrity and helps verify that the payload was generated by a trusted source, but it does not replace server-side validation of operation, destination, and data. You should still validate the URL, scope the signed fields, and enforce strict policies before making any network calls.