Cross Site Request Forgery in Django with Basic Auth
Cross Site Request Forgery in Django with Basic Auth
Cross Site Request Forgery (CSRF) is a client-side attack where an attacker tricks a victim’s browser into executing unwanted actions on a site where the victim is authenticated. When Basic Authentication is used, credentials are sent via the Authorization header on every request. This does not protect against CSRF at the application layer because browsers automatically include credentials for the target origin, including Authorization headers in certain scenarios (notably when using XMLHttpRequest/fetch with credentials or when the browser manages authentication for a realm). In Django, CSRF protection is designed for forms and requests that rely on session cookies, and it is not automatically effective when APIs use only HTTP Basic Auth. If a Django API endpoint accepts Basic Auth and also processes state-changing HTTP methods (POST, PUT, DELETE, PATCH) without requiring a CSRF token or an alternative anti-CSRF mechanism, an attacker can craft a malicious page that causes the victim’s browser to issue authenticated requests using the victim’s credentials.
A concrete scenario: a banking API endpoint /api/transfer/ accepts POST requests with Basic Auth and performs transfers based on JSON parameters. Because the browser sends the Authorization header automatically when the endpoint is accessed from a malicious site (e.g., via an img tag or a form submission with method spoofing via JavaScript), the request is authenticated. Django’s CSRF middleware does not reject the request because the middleware primarily checks for CSRF tokens in cookies and headers for same-origin form submissions; for API views that rely solely on Basic Auth, the developer must explicitly enforce CSRF protection or use alternative anti-CSRF controls. Without such controls, the attacker can cause unauthorized transfers or changes by leveraging the victim’s authenticated session. This risk is amplified if the endpoint does not validate the Origin header or implement per-request tokens, and if the application does not require explicit confirmation for sensitive actions.
In addition to CSRF, relying only on Basic Auth over HTTPS does not prevent CSRF; HTTPS protects credential confidentiality in transit but does nothing to stop unauthorized command execution from a victim’s browser. Developers often mistakenly assume that Basic Auth’s per-request credentials are sufficient to stop CSRF, but the browser’s credential handling for realms can cause it to send those headers automatically. Therefore, API endpoints in Django that use Basic Auth must explicitly address CSRF by either enforcing CSRF tokens for state-changing requests, requiring the SameSite attribute on any cookies used, validating Origin/Referer headers where appropriate, or adopting an anti-CSRF token mechanism tailored for APIs (such as double-submit cookie patterns or custom headers that are not automatically sent by browsers).
Basic Auth-Specific Remediation in Django
To mitigate CSRF when using Basic Auth in Django, explicitly enforce CSRF validation or implement alternative anti-CSRF controls for API views. For traditional form-based authentication, Django’s csrf_protect and middleware are sufficient, but for API endpoints using Basic Auth you must ensure that state-changing requests are protected by tokens or other mechanisms. One approach is to require CSRF tokens even for API requests, which can be done by using ensure_csrf_cookie and including the token in requests via a custom header. Below is a Django view example that explicitly ensures a CSRF cookie and validates it for a POST endpoint that uses Basic Auth parsing.
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
from django.http import JsonResponse
from django.contrib.auth import authenticate
from django.views.decorators.http import require_http_methods
import base64
@ensure_csrf_cookie
@csrf_protect
@require_http_methods(["POST"])
def transfer_funds(request):
# Parse Basic Auth
auth = request.META.get("HTTP_AUTHORIZATION", "")
if not auth.startswith("Basic "):
return JsonResponse({"error": "Unauthorized"}, status=401)
try:
decoded = base64.b64decode(auth.split(" ")[1]).decode("utf-8")
username, password = decoded.split(":", 1)
except Exception:
return JsonResponse({"error": "Bad credentials"}, status=400)
user = authenticate(request, username=username, password=password)
if user is None:
return JsonResponse({"error": "Invalid credentials"}, status=401)
# Validate CSRF token for state-changing action
# The client must include the CSRF token in header X-CSRFToken
csrf_token = request.META.get("HTTP_X_CSRFTOKEN")
if not csrf_token or not request.csrf_processors(request).process_view(request, None, (), {}):
# In practice, use proper CSRF validation; this is illustrative
return JsonResponse({"error": "CSRF token missing or invalid"}, status=403)
# Perform transfer logic here
return JsonResponse({"status": "ok"})
For API-first approaches, prefer using a double-submit cookie pattern where a custom header (e.g., X-CSRFToken) is required and its value must match the CSRF cookie. This works well with Basic Auth because the cookie is set via ensure_csrf_cookie and the header is provided by the client for each state-changing request. Django’s CsrfViewMiddleware can be kept enabled, and you can exempt non-API views while protecting API endpoints with explicit decorators or by organizing API URLs under a router that enforces CSRF. Below is an example of a middleware-style check in a view that validates the header against the cookie without relying on session authentication.
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import get_token
def csrf_cookie_view(request):
# Ensure CSRF cookie is set for the client
get_token(request)
return JsonResponse({"csrf_set": True})
@csrf_exempt # We handle validation manually in this example
from django.views.decorators.http import require_http_methods
import base64
@require_http_methods(["POST"])
def secure_transfer(request):
auth = request.META.get("HTTP_AUTHORIZATION", "")
if not auth.startswith("Basic "):
return JsonResponse({"error": "Unauthorized"}, status=401)
try:
decoded = base64.b64decode(auth.split(" ")[1]).decode("utf-8")
username, password = decoded.split(":", 1)
except Exception:
return JsonResponse({"error": "Bad credentials"}, status=400)
# Double-submit cookie pattern: compare header and cookie
csrf_header = request.META.get("HTTP_X_CSRFTOKEN")
csrf_cookie = request.COOKIES.get("csrftoken")
if not csrf_header or csrf_header != csrf_cookie:
return JsonResponse({"error": "CSRF token mismatch"}, status=403)
# Proceed with business logic
return JsonResponse({"status": "success"})
Additionally, ensure that Basic Auth is only used over HTTPS and consider combining it with SameSite cookie attributes for any session cookies your application uses. For APIs, evaluate whether token-based authentication (e.g., bearer tokens) with explicit CSRF mitigation is more appropriate than Basic Auth. Always validate and sanitize all inputs and enforce strict CORS policies to reduce the attack surface for CSRF and other injection-based attacks.