Dns Cache Poisoning in Flask with Basic Auth
Dns Cache Poisoning in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
DNS cache poisoning (also known as DNS spoofing) occurs when an attacker inserts false DNS records into a resolver’s cache, causing a client to be directed to a malicious host. In a Flask application that relies on Basic Authentication, this can have compounding effects because the client may unknowingly send credentials to an attacker-controlled endpoint.
Consider a Flask service that authenticates users via HTTP Basic Auth and then makes outbound HTTP requests to a backend API using a hostname that is resolved via DNS. If a resolver used by the client or server returns a poisoned record, the outbound request from Flask may be sent to an IP controlled by an attacker. Because Flask includes the Authorization header with the request, credentials can be exfiltrated or the attacker can perform actions on behalf of the client depending on the trust model.
The risk is not that Flask itself caches DNS in an unsafe way by default; the issue arises from the runtime environment’s DNS resolution combined with how the application uses Basic Auth. For example, if Flask makes requests with the requests library to a URL like https://api.internal.example.com/resource, and api.internal.example.com resolves to a malicious IP, the Authorization header is still sent to that malicious server. An attacker can then harvest credentials or inject false responses, leading to further compromise.
In a black-box scan, middleBrick tests for SSRF and external network interactions that could be influenced by DNS manipulation. When Basic Auth is present, findings related to insufficient hostname validation or missing certificate checks become higher severity because credentials are at risk of being sent to unintended endpoints.
Basic Auth-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring that Basic Auth credentials are only sent to verified, expected endpoints and that hostname resolution is not left to uncontrolled external resolvers. Below are concrete Flask patterns to reduce risk when using HTTP Basic Auth.
1. Pin hostnames with requests using a custom adapter or verify against an allowlist
Instead of relying solely on DNS at runtime, validate the hostname or IP against an allowlist before making outbound calls. This prevents a poisoned DNS entry from redirecting credentials.
import requests
from requests.adapters import HTTPAdapter
class HostnameVerificationAdapter(HTTPAdapter):
def __init__(self, allowed_hosts=None, *args, **kwargs):
self.allowed_hosts = set(allowed_hosts or [])
super().__init__(*args, **kwargs)
def send(self, request, **kwargs):
if request.url.lower().startswith('https://'):
hostname = request.url.split('/')[2].split(':')[0]
if hostname not in self.allowed_hosts:
raise ConnectionError(f'Hostname not allowed: {hostname}')
return super().send(request, **kwargs)
session = requests.Session()
session.mount('https://', HostnameVerificationAdapter(allowed_hosts=['api.trusted.example.com']))
session.get('https://api.trusted.example.com/resource', auth=('user', 'password'))
2. Use certificate pinning or strict TLS verification
Ensure that SSL/TLS verification is enabled and consider certificate pinning for critical services. This prevents an attacker with a poisoned DNS entry from using a valid but unauthorized certificate unless they also possess the pinned cert or key.
import requests
response = requests.get(
'https://api.internal.example.com/resource',
auth=('user', 'password'),
verify='/path/to/ca-bundle.pem' # or a pinned cert file
)
3. Avoid embedding credentials in URLs; use headers instead
Credentials in URLs can be logged more easily and may be more susceptible to exposure via DNS-based redirection. Use the Authorization header explicitly and avoid constructing URLs with embedded user:pass.
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/proxy')
def proxy():
username = request.authorization.username
password = request.authorization.password
# Explicit header usage, no credentials in URL
resp = requests.get(
'https://api.internal.example.com/resource',
headers={'Authorization': f'Basic {b64encode(f"{username}:{password}".encode()).decode()}'}
)
return resp.content
4. Validate and sanitize target hostnames from user input
If your Flask app accepts hostnames or URLs from users, strictly validate them to prevent open redirects or SSRF that could bypass DNS expectations.
from urllib.parse import urlparse
def is_safe_hostname(url, allowed_domains):
parsed = urlparse(url)
return parsed.hostname and any(parsed.hostname.endswith(d) for d in allowed_domains)
if is_safe_hostname(user_url, ['example.com', 'trusted.org']):
requests.get(url, auth=('user', 'password'))