Dns Cache Poisoning in Flask with Hmac Signatures
Dns Cache Poisoning in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
DNS cache poisoning attacks aim to corrupt resolver caches so that a domain name returns an attacker-controlled IP. In Flask applications that rely on external services, the framework itself does not manage DNS; the runtime environment’s resolver does. If a Flask app uses HMAC signatures only for application-level message integrity and does not also enforce strict hostname verification and certificate validation, an attacker who can poison DNS may redirect traffic to a malicious host that presents a valid certificate for the poisoned domain.
Consider a Flask service that discovers an upstream API hostname via DNS and then validates requests using HMAC signatures. The signature typically covers method, path, headers, and a timestamp, but if the verification logic does not explicitly bind the expected hostname, an attacker who successfully poisons DNS to point api.partner.example.com to a server they control can obtain valid HMAC-signed requests by replaying captured traffic or crafting new ones with the same key material. The Flask app may accept these requests as authentic because the signature matches, even though the network endpoint is no longer the intended partner. This is not a flaw in HMAC itself—HMAC correctly verifies integrity and authenticity of the signed data—but a contextual weakness where identity binding is missing.
In practice, this risk increases when applications embed hostnames inside signed payloads without enforcing them during verification, or when they trust DNS at connection time without revalidation. For example, if a client resolves a hostname once and caches it locally, a poisoned cache can persist across requests. MiddleBrick’s LLM/AI security and standard endpoint scanning checks can help detect missing hostname binding and weak transport validation by correlating spec definitions with runtime behavior, highlighting where identity is not cryptographically pinned to the endpoint.
To illustrate a vulnerable pattern, here is a Flask route that signs a request but does not explicitly verify the target hostname before trusting the signature:
import hashlib
import hmac
import time
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
SHARED_SECRET = b'super-secret-key'
def sign_payload(method, path, timestamp, body):
message = f'{method}\n{path}\n{timestamp}\n{body}'.encode()
return hmac.new(SHARED_SECRET, message, hashlib.sha256).hexdigest()
@app.route('/call-partner', methods=['POST'])
def call_partner():
data = request.get_data(as_text=True)
timestamp = str(int(time.time()))
signature = sign_payload('POST', '/api/data', timestamp, data)
headers = {
'X-API-Signature': signature,
'X-Timestamp': timestamp,
'Content-Type': 'application/json'
}
# Vulnerable: DNS resolved once, no hostname verification on response
resp = requests.post('https://api.partner.example.com/api/data', data=data, headers=headers, verify=True)
return jsonify({'status': resp.status_code, 'resp': resp.text})
In this example, verify=True ensures TLS certificate validation, but if DNS is poisoned and the attacker presents a valid certificate for api.partner.example.com, the request is accepted. The missing piece is explicit hostname confirmation after TLS handshake and binding the signature to the resolved hostname, not just the string used during signing.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
Remediation focuses on ensuring that HMAC verification includes explicit hostname checks and that runtime DNS resolution is treated as an untrusted input. Below are concrete code fixes for Flask applications that use HMAC signatures.
1. Always verify the hostname as part of signature validation. Include the expected hostname in the signed string or validate it after verification. Prefer using an allowlist of hostnames and reject any response that does not match the expected hostname exactly.
2. Re-validate hostname on each request if DNS is dynamic, and avoid long-lived cached hostnames in client logic. Use certificate pinning or public key pinning where appropriate to reduce reliance on DNS-based identity.
3. Use a strict timestamp window to prevent replay attacks, and include nonce or unique request identifiers where applicable.
Here is a hardened version of the previous example with hostname binding and verification:
import hashlib
import hmac
import time
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
SHARED_SECRET = b'super-secret-key'
EXPECTED_HOST = 'api.partner.example.com'
ALLOWED_HOSTS = {EXPECTED_HOST}
def sign_payload(method, host, path, timestamp, nonce, body):
message = f'{method}\n{host}\n{path}\n{timestamp}\n{nonce}\n{body}'.encode()
return hmac.new(SHARED_SECRET, message, hashlib.sha256).hexdigest()
def verify_signature(method, host, path, timestamp, nonce, body, signature):
expected = sign_payload(method, host, path, timestamp, nonce, body)
return hmac.compare_digest(expected, signature)
@app.route('/call-partner', methods=['POST'])
def call_partner():
data = request.get_data(as_text=True)
timestamp = str(int(time.time()))
nonce = 'unique-per-request' # In practice, use a cryptographically random value
signature = sign_payload('POST', EXPECTED_HOST, '/api/data', timestamp, nonce, data)
headers = {
'X-API-Signature': signature,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'Content-Type': 'application/json'
}
# Enforce hostname verification: ensure the resolved host matches expected
resp = requests.post(f'https://{EXPECTED_HOST}/api/data', data=data, headers=headers, verify=True, timeout=5)
# Additional safeguard: confirm response hostname (if inspecting redirect or alternate endpoint)
if resp.url.startswith(f'https://{EXPECTED_HOST}'):
# Validate timestamp window to mitigate replay (e.g., within 5 minutes)
# For brevity, nonce uniqueness and short timestamp window provide replay protection
return jsonify({'status': resp.status_code, 'resp': resp.text})
else:
return jsonify({'error': 'unexpected host'}), 400
In this remediation, the hostname is part of the signed payload and explicitly verified before use. The request is sent only to the expected host, and the response URL is checked to ensure no redirect to an untrusted location. For production use, generate a secure random nonce per request and store used nonces for replay detection within the timestamp window.
When integrating with OpenAPI/Swagger workflows, ensure that spec-derived hosts and paths are cross-checked against runtime behavior. MiddleBrick’s spec analysis can highlight mismatches between declared hosts and runtime calls, which complements HMAC-based integrity by reducing the attack surface where identity binding might be overlooked.