HIGH ssrfdjangohmac signatures

Ssrf in Django with Hmac Signatures

Ssrf in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Server-Side Request Forgery (SSRF) in Django when HMAC signatures are used for request authentication can arise when an application builds a signed URL using a shared secret and then causes the server to make an HTTP request to that same URL. If the destination URL is supplied by an attacker and the signing logic does not validate or restrict targets, the server can be tricked into signing arbitrary internal endpoints, effectively turning the Django app into an SSRF proxy that bypasses destination-based network restrictions.

Consider an endpoint that accepts a resource path and produces a signed URL for a backend service. If the signing function does not enforce a strict allowlist of hosts and the resulting URL is later fetched by the server (or by an outbound HTTP client), an attacker can supply an internal address such as http://169.169.169.200/latest/meta-data/iam/security-credentials/. The signature will validate because the shared secret is known to the server, but the request reaches an internal metadata service that would not be exposed externally. This pattern is common in architectures where Django generates presigned URLs for internal service-to-service calls or for temporary access to storage backends.

In a typical implementation, the vulnerability chain involves: (1) an input parameter that specifies a target host or path; (2) a signing routine that uses HMAC (for example, hash-based message authentication with SHA256) to create a token appended to the query string; (3) an outbound request that uses the signed URL without revalidating the target; and (4) an implicit trust that the signature alone ensures safety. Because the signature is computed over the attacker-controlled portion, the server treats the request as legitimate. The risk is compounded if the signing scope is too broad, the secret is leaked, or the application does not enforce strict host or scheme allowlists.

middleBrick detects this pattern by analyzing OpenAPI/Swagger specs with full $ref resolution and correlating them with runtime behavior, including HMAC-based authentication schemes. It flags cases where endpoints accept free-form URLs that are later used in server-side requests and where signature validation does not sufficiently restrict targets. Findings align with OWASP API Top 10 and common misconfigurations around URL construction and signature scope.

Hmac Signatures-Specific Remediation in Django — concrete code fixes

To mitigate SSRF when using HMAC signatures in Django, enforce strict allowlists for hosts and schemes, avoid signing user-supplied full URLs, and validate paths against a known set of internal services. Below are concrete, secure patterns.

1. Sign only safe, known paths — not full URLs

Instead of allowing an attacker to specify the full target URL, have the client provide a short, application-defined key (e.g., service_alias) and construct the URL server-side from a controlled mapping. Sign the key and any non-trusted parameters (such as expiration), not the host.

import hmac
import hashlib
import time
import urllib.parse
from django.conf import settings
from django.http import JsonResponse

def generate_signed_token(service_alias: str, expires: int, secret: bytes) -> str:
    payload = f"{service_alias}|{expires}"
    signature = hmac.new(secret, payload.encode("utf-8"), hashlib.sha256).hexdigest()
    return f"{payload}|{signature}"

def verify_signed_token(token: str, secret: bytes) -> tuple[str, int] | None:
    try:
        payload, received_sig = token.rsplit("|", 1)
        expected_sig = hmac.new(secret, payload.encode("utf-8"), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected_sig, received_sig):
            return None
        service_alias, expires_str = payload.split("|")
        expires = int(expires_str)
        if int(time.time()) > expires:
            return None
        return service_alias, expires
    except Exception:
        return None

# Controlled mapping of alias to internal base URL
SERVICE_URLS = {
    "reports": "https://internal-reports.example.com/api/v1",
    "assets": "https://internal-assets.example.com/files",
}

def build_signed_url(request):
    alias = request.GET.get("alias")
    if alias not in SERVICE_URLS:
        return JsonResponse({"error": "invalid alias"}, status=400)
    expires = int(time.time()) + 300  # 5 minutes
    secret = settings.SECRET_KEY.encode("utf-8")
    token = generate_signed_token(alias, expires, secret)
    base = SERVICE_URLS[alias]
    # safe: we append only path/query controlled by the app, not the host
    target = urllib.parse.urljoin(base, f"/data?token={token}")
    return JsonResponse({"url": target})

2. Validate host and scheme on the server before any outbound call

If you must accept a URL from an external source, validate the parsed host and scheme against an explicit allowlist before using it, even if it is signed. Do not rely on the signature to indicate safety.

import hmac
import hashlib
import urllib.parse
from urllib.parse import urlparse
from django.http import JsonResponse

ALLOWED_HOSTS = {"api.internal.example.com"}
ALLOWED_SCHEMES = {"https"}
SECRET = b"your-django-secret-key"

def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False
    if parsed.hostname not in ALLOWED_HOSTS:
        return False
    return True

def sign_url(url: str, secret: bytes) -> str:
    parsed = urlparse(url)
    query = parsed.query
    payload = query if query else ""
    signature = hmac.new(secret, payload.encode("utf-8"), hashlib.sha256).hexdigest()
    return f"{url}&sig={signature}"

def verify_and_fetch(request):
    target = request.GET.get("url")
    if not target or not is_safe_url(target):
        return JsonResponse({"error": "unsafe target"}, status=400)
    token = request.GET.get("sig")
    parsed = urlparse(target)
    expected_payload = parsed.query
    expected_sig = hmac.new(SECRET, expected_payload.encode("utf-8"), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected_sig, token):
        return JsonResponse({"error": "invalid signature"}, status=400)
    # Proceed with outbound request using target
    return JsonResponse({"status": "verified"})

3. Apply middleware or decorator checks for signature scope

Ensure that the set of data included in the HMAC is limited and does not inadvertently endorse dangerous parameters such as redirect URLs or arbitrary hosts. Prefer signing a short path and a timestamp, and reconstruct the full URL internally.

These practices reduce the attack surface so that even if an attacker can influence the signed payload, they cannot direct the request to unintended internal services, thereby mitigating SSRF in the context of HMAC-signed requests in Django.

Related CWEs: ssrf

CWE IDNameSeverity
CWE-918Server-Side Request Forgery (SSRF) CRITICAL
CWE-441Unintended Proxy or Intermediary (Confused Deputy) HIGH

Frequently Asked Questions

Why is signing a full user-supplied URL risky even with HMAC in Django?
Because a valid signature on a malicious URL (e.g., pointing to an internal metadata service) can make the server trust and forward requests to internal endpoints, enabling SSRF. Sign only safe, known components (such as a service alias) and validate the target host and scheme independently.
What is a safer alternative to passing full URLs for HMAC signing in Django?
Use an allowlist mapping (e.g., service_alias -> base URL) and sign only the alias and non-trusted parameters like expiration. Construct the final URL server-side, avoiding the need to sign or trust user-provided hosts.