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 ID | Name | Severity |
|---|---|---|
| CWE-918 | Server-Side Request Forgery (SSRF) | CRITICAL |
| CWE-441 | Unintended Proxy or Intermediary (Confused Deputy) | HIGH |