Ssrf Server Side in Django with Basic Auth
Ssrf Server Side in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
Server-side request forgery (SSRF) in Django becomes notably more complex when endpoints are protected with HTTP Basic Authentication. Basic Auth transmits credentials in each request, encoded in an Authorization header rather than a session cookie. When a backend service or internal endpoint is only reachable with a valid Basic Auth token, developers may be tempted to embed that token in client-side code or in requests generated by the server. Doing so can expose the credentials to SSRF: an attacker who can control an outbound URL may force the application to include the Authorization header when visiting internal or restricted hosts, revealing private services that would otherwise be invisible from the internet.
Consider a Django view that fetches data from a metadata service and appends a hardcoded Basic Auth header:
import base64
import requests
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET"])
def fetch_metadata(request):
url = request.GET.get("url")
if not url:
return JsonResponse({"error": "url parameter required"}, status=400)
token = base64.b64encode(b"admin:SuperSecret123").decode()
headers = {"Authorization": f"Basic {token}"}
resp = requests.get(url, headers=headers, timeout=5)
return JsonResponse({"status_code": resp.status_code, "body": resp.text[:200]})
If an attacker can supply the url parameter, they can point it to an internal address such as http://169.254.169.254/latest/meta-data/ (AWS instance metadata) or to an internal admin interface on 127.0.0.1. The injected Authorization header may grant access to otherwise restricted internal endpoints, turning an SSRF into a privileged internal reconnaissance path. Complications arise when the upstream service uses IP-based allowlists; the request appears to come from the Django host, but the Basic credentials may be required to reach a protected resource that would otherwise reject the call.
SSRF in this context can lead to several outcomes: retrieving sensitive metadata, interacting with internal management APIs, or bypassing network segregation. Because Basic Auth credentials are static, embedding them in application code or environment variables that the Django process can access increases the blast radius if SSRF is possible. MiddleBrick’s unauthenticated scan can surface endpoints that accept attacker-controlled URLs and include Authorization headers, highlighting the exposure without needing credentials to begin an assessment.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on preventing untrusted input from influencing outbound requests, avoiding hardcoded credentials in request-building code, and applying the principle of least privilege to any downstream services.
- Do not forward user-supplied URLs to requests that include Authorization headers. If you must call internal services, use a fixed, tightly scoped configuration for the target host and do not concatenate user input into the URL.
- Use Django settings and environment variables for credentials, and avoid generating Basic Auth headers on a per-request basis for outbound calls. Instead, rely on service accounts with minimal permissions, and rotate credentials regularly.
- Implement allowlists for protocols and hosts. For example, if you only need to call a known internal API, validate the parsed hostname and scheme before making the request.
Here is a safer pattern that avoids embedding Basic Auth in dynamic requests:
import requests
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
# settings.py or environment:
# DOWNSTREAM_USER = "svc_reader"
# DOWNSTREAM_PASS = "********"
# ALLOWED_HOSTS = {"internal-api.example.com"}
@require_http_methods(["GET"])
def safe_fetch_metadata(request):
target = request.GET.get("host")
if not target:
return JsonResponse({"error": "host parameter required"}, status=400)
# Strict validation: only allow a specific internal host
allowed = settings.ALLOWED_HOSTS
if target not in allowed:
return JsonResponse({"error": "host not allowed"}, status=403)
# Use a fixed destination; do not build URLs from user input
url = f"https://{target}/api/v1/metadata"
creds = (settings.DOWNSTREAM_USER, settings.DOWNSTREAM_PASS)
try:
resp = requests.get(url, auth=creds, timeout=5)
resp.raise_for_status()
except requests.RequestException:
return JsonResponse({"error": "unable to fetch"}, status=502)
return JsonResponse({"status_code": resp.status_code, "body": resp.text[:200]})
If you must add an Authorization header dynamically (for example, when integrating with multiple downstream services), validate the target host rigorously and avoid concatenating any part of the URL that could be controlled by an attacker. Prefer using mutual TLS or service-specific tokens scoped to a single consumer instead of Basic Auth when feasible. MiddleBrick’s GitHub Action can be added to your CI/CD pipeline to fail builds if a scan detects endpoints that combine user-controlled URLs with Authorization headers, helping you enforce these rules before deployment.