Dns Cache Poisoning in Django
How Dns Cache Poisoning Manifests in Django
DNS cache poisoning in Django applications typically occurs through improper handling of external HTTP requests and misconfigured DNS resolution. When Django applications make outbound requests to external services—whether for API calls, webhook delivery, or third-party integrations—they rely on DNS resolution that can be manipulated by attackers.
The most common Django-specific manifestation involves the requests library or Django's built-in django.core.mail module making outbound connections. If an application uses hardcoded domain names or relies on DNS responses without validation, an attacker who poisons the DNS cache can redirect traffic to malicious servers. For example, a Django application might use requests.get('https://api.thirdparty.com/data') where the DNS entry for api.thirdparty.com has been poisoned to point to an attacker-controlled IP.
Django's caching framework can also be exploited. If DNS resolution results are cached at the application level without proper TTL handling, a poisoned DNS entry remains effective until the cache expires. This is particularly dangerous in production environments where Django applications often run behind load balancers with their own DNS caching layers.
Another Django-specific vector involves the use of ALLOWED_HOSTS settings. While this setting prevents HTTP Host header attacks, it doesn't validate the actual resolved IP addresses. An attacker could poison DNS to make a legitimate domain resolve to a malicious IP, and Django would accept it if the domain is in ALLOWED_HOSTS.
Middleware that performs external requests is especially vulnerable. Custom authentication middleware that validates tokens by calling external services, or middleware that fetches configuration from remote servers, can all be compromised through DNS cache poisoning. The issue compounds when these requests are made during application startup, as the poisoned entries persist throughout the application lifecycle.
Webhooks present another significant risk. Django applications often implement webhook handlers that make outbound requests to verify signatures or acknowledge receipt. If these verification requests use DNS names without IP pinning, an attacker can intercept and manipulate the verification process by controlling the resolved destination.
Django-Specific Detection
Detecting DNS cache poisoning in Django requires both runtime monitoring and static code analysis. The most effective approach combines automated scanning with manual code review of network-related components.
middleBrick's API security scanner includes specific checks for DNS-related vulnerabilities in Django applications. The scanner examines outbound request patterns, looking for hardcoded domain names, lack of IP pinning, and improper DNS caching configurations. It tests whether your application properly validates resolved IP addresses against expected ranges and whether it implements DNSSEC validation where applicable.
Static analysis should focus on identifying all external network calls in your Django codebase. Search for patterns like:
import requests
from django.core.mail import send_mail
from django.conf import settings
# High-risk patterns
def risky_function():
# Hardcoded domain without validation
response = requests.get('https://api.example.com/data')
# No IP pinning or DNSSEC
send_mail('Subject', 'Body', '[email protected]',
['[email protected]'], fail_silently=False)
middleBrick's scanner specifically flags these patterns and tests them against controlled DNS manipulation scenarios. The scanner attempts to resolve your configured external domains and checks whether the application properly handles unexpected IP addresses or DNS resolution failures.
Runtime detection involves monitoring DNS resolution logs and implementing request validation middleware. You can add middleware that logs all external requests with their resolved IP addresses and compares them against expected ranges:
class DNSSecurityMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.expected_ips = {
'api.example.com': ['192.168.1.1', '192.168.1.2']
}
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
# Check external requests made by the view
pass
The scanner also tests for SSRF (Server-Side Request Forgery) vulnerabilities that often accompany DNS issues, as both involve improper handling of external requests. middleBrick's comprehensive approach includes testing for both the poisoning vector and the exploitation path.
Django-Specific Remediation
Remediating DNS cache poisoning in Django applications requires a multi-layered approach that combines proper configuration, validation, and secure coding practices. The foundation is implementing IP pinning and DNSSEC validation for all external requests.
For HTTP requests using the requests library, implement a custom transport adapter that validates resolved IP addresses:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import socket
import ipaddress
class ValidatingHTTPAdapter(HTTPAdapter):
def __init__(self, expected_ips=None, *args, **kwargs):
self.expected_ips = expected_ips or {}
super().__init__(*args, **kwargs)
def send(self, request, **kwargs):
# Resolve and validate the hostname
hostname = request.url.split('/')[2]
try:
resolved_ips = socket.gethostbyname_ex(hostname)[2]
if hostname in self.expected_ips:
allowed_ips = self.expected_ips[hostname]
if not any(ip in allowed_ips for ip in resolved_ips):
raise ValueError(f"DNS resolution for {hostname} returned unexpected IPs")
except Exception as e:
raise ValueError(f"DNS validation failed: {e}")
return super().send(request, **kwargs)
# Usage
session = requests.Session()
transport = ValidatingHTTPAdapter(
expected_ips={'api.example.com': ['203.0.113.1', '203.0.113.2']}
)
session.mount('https://', transport)
response = session.get('https://api.example.com/data')
For Django's email functionality, configure DNSSEC validation and implement IP pinning at the SMTP level:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your_username'
EMAIL_HOST_PASSWORD = 'your_password'
# Custom email backend with DNS validation
class SecureSMTPSender:
def __init__(self, host, port, *args, **kwargs):
self.host = host
self.port = port
# Validate DNS resolution during initialization
self.verified_ips = self.resolve_and_verify(host)
def resolve_and_verify(self, hostname):
# Implement DNSSEC validation here
# Return verified IP addresses
pass
Implement Django middleware that validates all outbound requests:
class SecureRequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.allowed_domains = {
'api.example.com': ['203.0.113.1'],
'webhook.example.com': ['198.51.100.1']
}
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
# Wrap external request functions with validation
pass
Configure Django's caching framework to use appropriate TTL values and implement cache poisoning detection:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'TIMEOUT': 300, # 5 minutes for DNS resolution cache
}
}
# Custom cache backend with poisoning detection
class SecureCacheBackend:
def set(self, key, value, timeout=300):
# Validate cached DNS entries
if 'dns:' in key:
# Implement validation logic
pass
super().set(key, value, timeout)
Finally, implement comprehensive logging and monitoring to detect anomalous DNS resolution patterns:
import logging
logger = logging.getLogger(__name__)
class DNSMonitoringMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.resolution_history = {}
def __call__(self, request):
response = self.get_response(request)
return response
def log_dns_resolution(self, domain, resolved_ips):
# Track DNS resolution patterns
if domain not in self.resolution_history:
self.resolution_history[domain] = []
self.resolution_history[domain].append({
'timestamp': time.time(),
'ips': resolved_ips
})
# Alert on unexpected changes
if len(self.resolution_history[domain]) > 5:
# Analyze for anomalies
pass