Man In The Middle in Django with Basic Auth
Man In The Middle in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
When Django is configured to use HTTP Basic Authentication without transport-layer protection, a Man In The Middle (MitM) can intercept credentials in transit. Basic Auth encodes the username and password with Base64, which is trivial to decode; it does not encrypt the credentials. If an attacker is able to observe the network traffic—such as on an open Wi-Fi network, a compromised router, or through a vulnerable proxy—they can capture the Authorization header and reuse it to impersonate the legitimate user.
Django itself does not introduce the encryption weakness; it relies on the transport (typically HTTPS/TLS) to protect requests. In a MitM scenario where TLS is absent or improperly configured (e.g., self-signed certificates accepted without verification, or TLS terminated incorrectly), the Base64-encoded credentials are exposed in plaintext. Compounded risks include session fixation if the same credentials are reused across requests and weak password policies that make credential guessing or brute-force easier once the token is captured.
Even when Django uses Basic Auth via HttpRequest.authorization or an authentication class, the framework processes the credentials only after they arrive. If the channel is unencrypted, the protection boundary is broken before Django’s authentication logic runs. This makes the combination of Basic Auth and an unverified or missing TLS layer particularly dangerous: the secret is transmitted in every request, and an interceptor can replay it indefinitely until the password is rotated or the token is invalidated.
To detect this class of risk, scanning should validate that authentication mechanisms are only used over cryptographically secured channels and that certificate validation is enforced on the client side. Tools that perform unauthenticated, black-box checks can surface whether credentials are transmitted in a manner that could be exposed, prompting a review of transport security rather than the authentication scheme itself.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring credentials are never transmitted in the clear and that the application enforces strong transport security. The primary fix is to mandate HTTPS for all requests, including those that carry Authorization headers. In Django, you can enforce this at the project level and within authentication logic.
Enforce HTTPS site-wide
In your production settings, ensure the following settings are configured. These settings instruct Django to respect proxy headers for HTTPS and to redirect all HTTP requests to HTTPS.
import os
# settings.py
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Serve Basic Auth only over HTTPS
If you must use HTTP Basic Authentication, ensure it is only accepted on endpoints that require it and that those endpoints are guarded by HTTPS. Below is an example of a Django view that uses HTTP Basic Authentication safely, assuming HTTPS is enforced globally.
import base64
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View
class SecureBasicAuthView(View):
# In production, store credentials securely (e.g., environment variables or a secrets manager)
VALID_USERNAME = os.getenv('BASIC_AUTH_USER')
VALID_PASSWORD = os.getenv('BASIC_AUTH_PASS')
def dispatch(self, request, *args, **kwargs):
auth_header = request.META.get('HTTP_AUTHORIZATION')
if not auth_header or not auth_header.startswith('Basic '):
return self._unauthorized()
encoded = auth_header.split(' ', 1)[1]
try:
decoded = base64.b64decode(encoded).decode('utf-8')
except Exception:
return self._unauthorized()
if ':' not in decoded:
return self._unauthorized()
username, password = decoded.split(':', 1)
if self.is_valid_credentials(username, password):
return self.handle_request(request, *args, **kwargs)
return self._unauthorized()
def is_valid_credentials(self, username, password):
return username == self.VALID_USERNAME and password == self.VALID_PASSWORD
def handle_request(self, request, *args, **kwargs):
# Your protected logic here
return HttpResponse('Authenticated successfully')
def _unauthorized(self):
return HttpResponse('Unauthorized', status=401, headers={'WWW-Authenticate': 'Basic realm="Secure Area"'})
Rotate credentials and monitor usage
Even with HTTPS, treat Basic Auth credentials as high-risk. Rotate them regularly, avoid embedding them in client-side code or logs, and monitor for unusual request patterns that may indicate replay or interception. Combine with IP allowlists or short-lived tokens where feasible to reduce exposure.