Dns Rebinding in Django with Hmac Signatures
Dns Rebinding in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
DNS Rebinding is a network-based attack where an attacker causes a victim’s browser to resolve a domain name to an IP address under the attacker’s control after the initial request. In Django, when HMAC signatures are used to validate the origin or integrity of a request (e.g., to verify a webhook or an API call), a vulnerable implementation can mistakenly trust the HTTP Host header or other request metadata that can be altered via DNS Rebinding.
Consider a Django service that accepts signed requests and verifies an HMAC signature to ensure the request comes from a trusted source. If the application uses the Host header or an Origin header as part of the data that is signed or compared, an attacker can control the DNS response to make the client send requests to an IP address that the application mistakenly considers valid. The browser will send the request with the original Host header (e.g., trusted.example.com) due to DNS cache behavior, but the request arrives at an attacker-controlled IP. The attacker can then observe or manipulate the request path, and if the signature validation logic does not validate the canonical hostname or IP directly, it may incorrectly accept the request.
For example, suppose a Django view validates an HMAC signature using a shared secret and checks that the Host header matches an allowed list. An attacker registers a domain attacker.com and resolves it to a server they control. The victim’s browser is tricked into making a request with Host: trusted.example.com, but the TCP connection terminates at the attacker’s server. If the signature verification only checks the Host header value and does not additionally verify the actual destination IP or enforce strict hostname-to-IP binding, the attacker can relay or modify the request in a way that bypasses intended origin checks.
This becomes especially relevant in microservice or on-premise setups where internal services rely on predictable hostnames and weak binding between hostname, IP, and port. The Django application may also expose an endpoint that returns sensitive data or triggers an action when a valid HMAC signature is provided. If the signature validation does not incorporate strict checks—such as comparing the canonical hostname or IP against a whitelist, using a nonce or timestamp, and rejecting ambiguous Host headers—an attacker can exploit DNS Rebinding to perform unauthorized actions or leak information via the signed request flow.
To detect this class of issue during scanning, middleBrick performs unauthenticated checks that validate whether the application’s HMAC-based integrity checks are tied strictly to expected network endpoints and whether the application allows ambiguous origin indicators. The scanner does not modify your network or DNS setup; it only reports findings and provides remediation guidance to help you harden the validation logic.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate DNS Rebinding risks when using HMAC signatures in Django, ensure that signature validation does not rely solely on mutable headers such as Host or Origin, and always validate the destination endpoint explicitly. Below are concrete code examples that demonstrate a secure approach.
Example 1: HMAC validation with strict host and IP checks
Use a hardcoded or configuration-driven list of allowed hosts and validate the request’s destination address. Do not trust the Host header for security decisions.
import hmac
import hashlib
import time
from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.views.decorators.http import require_POST
ALLOWED_HOSTS = {"api.trusted.example.com": ["192.0.2.10"]} # hostname -> list of allowed IPs
TOLERANCE_SECONDS = 30 # for timestamp replay protection
def verify_hmac_signature(
request: HttpRequest,
secret: bytes,
payload: bytes,
received_signature: str,
request_timestamp: int,
) -> bool:
if abs(time.time() - request_timestamp) > TOLERANCE_SECONDS:
return False
message = payload + str(request_timestamp).encode("utf-8")
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)
@require_POST
def webhook_view(request):
# Always use a direct destination check; do not rely on Host for security
dest_host = "api.trusted.example.com"
dest_ip = "192.0.2.10"
if request.get_host().lower() != dest_host:
return HttpResponseForbidden("Host not allowed")
# Optionally validate the resolved destination IP (requires socket integration in production)
# Example assumes you enforce via deployment or middleware that the target IP matches
signature_header = request.META.get("HTTP_X_SIGNATURE")
timestamp_header = request.META.get("HTTP_X_TIMESTAMP")
if not signature_header or not timestamp_header:
return HttpResponseForbidden("Missing signature or timestamp")
try:
timestamp = int(timestamp_header, 10)
except ValueError:
return HttpResponseForbidden("Invalid timestamp")
payload = request.body
if not verify_hmac_signature(
request=request,
secret=settings.WEBHOOK_SECRET.encode("utf-8"),
payload=payload,
received_signature=signature_header,
request_timestamp=timestamp,
):
return HttpResponseForbidden("Invalid signature")
# Process the verified request
return HttpResponse("OK")
Example 2: Using a request-scoped nonce and canonical host/IP
Include a nonce or timestamp signed by the server and require the client to echo it. Validate the destination IP programmatically where possible.
import socket
import hmac
import hashlib
import time
from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
def get_canonical_destination():
# This should resolve to the IP your service expects; adjust for your deployment
return socket.gethostbyname("api.trusted.example.com")
def validate_destination_ip(request: HttpRequest) -> bool:
expected_ip = get_canonical_destination()
# In practice, you may inspect the connection’s peer IP or use X-Forwarded-For carefully
return True # placeholder: implement network-level checks as appropriate
def verify_signed_request(request: HttpRequest) -> bool:
nonce = request.META.get("HTTP_X_NONCE")
signature = request.META.get("HTTP_X_SIGNATURE")
if not nonce or not signature:
return False
# Ensure destination integrity before signature verification
if not validate_destination_ip(request):
return False
secret = settings.WEBHOOK_SECRET.encode("utf-8")
message = nonce.encode("utf-8")
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@require_POST
def secure_view(request):
if not verify_signed_request(request):
return HttpResponseForbidden("Invalid request")
return HttpResponse("Verified")
General best practices
- Do not use the Host header as the sole authority for access control; validate against a strict allowlist of hostnames and corresponding IPs.
- Enforce timestamp or nonce checks to prevent replay attacks across rebinding windows.
- Where feasible, perform destination IP validation at the network or middleware level to ensure the request arrives at the expected endpoint.
- Use HTTPS to prevent on-path manipulation of Host headers and to protect the integrity of the signed data.
These examples emphasize that HMAC signatures must be bound to a verifiable destination and not just to headers that can be altered via DNS Rebinding. middleBrick can help identify whether your current validation logic is vulnerable by checking for over-reliance on mutable headers and insufficient endpoint verification.