Dangling Dns in Django with Basic Auth
Dangling Dns in Django with Basic Auth — how this specific combination creates or exposes the vulnerability
A dangling DNS record occurs when a hostname (e.g., legacy.internal.example.com) still resolves to an IP address but is no longer actively managed or intentionally configured by the application owner. In a Django project that uses HTTP Basic Auth, this combination can unintentionally expose administrative or internal endpoints to external actors if the DNS entry points to a public-facing server while the Django app erroneously trusts the hostname for access control.
When Basic Auth is used in Django, access decisions often rely on a combination of credentials (username and password) and request metadata such as the HTTP Host header, referer, or resolved origin IP. If the Django configuration includes hostname-based allowlists (e.g., permitting access only from certain domains) and a dangling DNS record resolves to the same server, an attacker who can route traffic to that IP may bypass intended network-level restrictions. This happens because the application validates the hostname against the dangling record, which still resolves, instead of also enforcing strict credential validation and origin checks.
Consider a Django service that protects an internal admin interface with Basic Auth and also checks that requests originate from an internal domain like admin.internal.example.com. If admin.internal.example.com becomes a dangling DNS record pointing to the public server, an unauthenticated external request that manually sets the Host header to admin.internal.example.com might appear to originate from an allowed domain. Without robust server-side enforcement of authentication and strict hostname validation, the dangling record effectively becomes an unintended entry point.
Moreover, in environments where DNS changes lag behind infrastructure changes, developers might assume a hostname is internal-only, but if it still resolves, the assumption creates a false sense of security. Basic Auth protects the endpoint only if the client can provide valid credentials; a dangling DNS record does not weaken the authentication mechanism itself but can mislead developers into thinking network-level controls are sufficient. The risk is compounded if the view or middleware relies on request.get_host() to make authorization decisions without verifying that the host is truly authoritative for the service.
To detect this specific risk profile, middleBrick scans unauthenticated attack surfaces and validates assumptions about hostname-based controls alongside Basic Auth behavior. By cross-referencing OpenAPI/Swagger specifications with runtime requests, it identifies mismatches between intended access boundaries and actual DNS reachability, including dangling records that could be abused in conjunction with weak hostname checks.
Basic Auth-Specific Remediation in Django — concrete code fixes
Remediation focuses on ensuring that Basic Auth is the primary gatekeeper and that hostname-based checks are strict, explicit, and never relied upon alone. Always treat the Host header as untrusted for access control. Use Django’s built-in authentication mechanisms and avoid custom host allowlists for security decisions.
Example of secure Django Basic Auth using a decorator that enforces credentials without relying on hostname checks for access decisions:
from django.contrib.auth import authenticate
from django.http import HttpResponse, HttpResponseForbidden
from functools import wraps
def basic_auth_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.lower().startswith('basic '):
return HttpResponse('Unauthorized', status=401, headers={'WWW-Authenticate': 'Basic realm="Access"'})
import base64
try:
decoded = base64.b64decode(auth.split(' ', 1)[1]).decode('utf-8')
username, password = decoded.split(':', 1)
except Exception:
return HttpResponseForbidden()
user = authenticate(request, username=username, password=password)
if user is not None and user.is_active:
return view_func(request, *args, **kwargs)
return HttpResponseForbidden()
return _wrapped_view
@basic_auth_required
def admin_view(request):
return HttpResponse('Admin area')
If you must incorporate hostname checks, validate against a hardcoded set of canonical names and ensure DNS records are actively managed:
from django.http import HttpResponseForbidden
ALLOWED_HOSTS = {'api.example.com', 'app.example.com'}
def strict_host_check(view_func):
def _wrapped_view(request, *args, **kwargs):
host = request.get_host().split(':')[0]
if host not in ALLOWED_HOSTS:
return HttpResponseForbidden()
return view_func(request, *args, **kwargs)
return _wrapped_view
@basic_auth_required
@strict_host_check
def protected_view(request):
return HttpResponse('Secured')
Additionally, remove any dangling DNS records from your zones and verify that internal hostnames do not resolve to public endpoints. Use middleware to log unexpected Host headers for further investigation, but do not use them to grant access.
middleBrick’s LLM/AI Security checks and OpenAPI analysis can surface scenarios where hostname-based logic interacts with authentication mechanisms, providing prioritized findings and remediation guidance to address these edge cases.