Api Rate Abuse in Django with Hmac Signatures
Api Rate Abuse in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Rate abuse occurs when an attacker makes an excessive number of requests to an API endpoint, degrading availability or enabling brute-force, enumeration, or resource exhaustion attacks. In Django, combining HMAC signatures with rate limiting can inadvertently expose weaknesses if the implementation does not carefully protect both the integrity of the signature and the scope of rate enforcement.
HMAC signatures are designed to ensure authenticity and integrity by signing a canonical representation of the request (often including selected headers, the request path, query parameters, and timestamp). A typical pattern includes X-API-Key, X-Timestamp, and X-Signature headers. If rate limiting is applied naively—such as by IP only or solely on the endpoint path—it may be bypassed or abused when the signature mechanism leaks state or when the signing scope is too permissive. For example, an attacker who can vary a non‑sensitive query parameter (e.g., a filter ID) while keeping the signature valid (because the signature excludes that parameter) can generate many distinct signed requests that each fall under the same rate limit bucket, effectively bypassing protection.
Another common pitfall is timestamp tolerance. HMAC schemes often include a timestamp to prevent replay attacks, with a server‑side window (for instance, 5 minutes). If the rate limit window is misaligned with the timestamp window, an attacker can replay valid signed requests within the timestamp tolerance faster than the per‑minute threshold allows. Additionally, if the signature covers only the path and selected headers but not the request body or a nonce, an attacker may reuse a signed payload for multiple requests, especially in POST or PUT operations that are not idempotent.
Django middleware or view decorators that enforce rate limits based only on the resolved URL pattern can also be insufficient when HMAC signatures allow parameter manipulation that maps to the same endpoint. For instance, consider an endpoint like /api/v1/resources/{resource_id} where resource_id is not included in the signature scope. An attacker can iterate over resource IDs, each producing a distinct URL but sharing the same signed base, thereby distributing requests across many IDs to evade per‑client or per‑IP limits.
Furthermore, if the HMAC verification logic is implemented in a way that accepts slightly malformed or loosely validated inputs, an attacker can craft variations that bypass secondary checks. For example, failing to enforce a strict canonicalization order of signed headers or allowing multiple equivalent representations of the same timestamp can create ambiguity that leads to inconsistent rate enforcement. Without a tightly bounded signing scope that includes critical differentiating data (such as a nonce or a resource identifier), the combination of HMAC signatures and rate limiting can unintentionally permit high‑volume abuse while appearing to enforce security.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate rate abuse when using HMAC signatures in Django, align the signing scope with the rate‑limit scope, canonicalize inputs rigorously, and bind signatures to elements that prevent enumeration and replay. Below are concrete, production‑oriented examples that demonstrate a robust approach.
1. Canonical request construction and signature verification
Define a function that builds a canonical string from selected parts of the request that should be rate‑limited together. Include the HTTP method, path, a sorted subset of query parameters, a nonce or request ID (when present), and the timestamp. Verify the timestamp window and reject requests with excessive clock skew.
import hashlib
import hmac
import time
from django.http import HttpRequest, HttpResponseForbidden
from django.conf import settings
def verify_hmac(request: HttpRequest) -> bool:
api_key = request.META.get('HTTP_X_API_KEY')
timestamp = request.META.get('HTTP_X_TIMESTAMP')
signature = request.META.get('HTTP_X_SIGNATURE')
if not all([api_key, timestamp, signature]):
return False
# Enforce timestamp tolerance (e.g., 300 seconds)
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
# Build canonical string
method = request.method.upper()
path = request.get_full_path().split('?')[0] # exclude query for canonical base
# Include selected query params in sorted order to ensure consistency
query_items = sorted(request.GET.lists())
canonical = '|'.join([method, path] + [f'{k}={v[0]}' for k, v in query_items] + [f'ts={timestamp}'])
secret = settings.HMAC_SECRET.encode()
expected = hmac.new(secret, canonical.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
2. Rate limiting bound to the signed scope
Use a key for rate limiting that incorporates the API key and the canonical scope used for signing. This ensures that variations which change only non‑signed parameters cannot bypass limits.
from django.core.cache import caches
def rate_limit_key(request: HttpRequest) -> str:
api_key = request.META.get('HTTP_X_API_KEY')
# Include the same canonical components used in HMAC verification
path = request.get_full_path().split('?')[0]
query_items = sorted(request.GET.lists())
scope = '|'.join([path] + [f'{k}={v[0]}' for k, v in query_items])
return f'ratelimit:{api_key}:{scope}'
def check_rate_limit(request: HttpRequest, limit: int = 60, window: int = 60) -> bool:
from django.core.cache import caches
cache = caches['default']
key = rate_limit_key(request)
current = cache.get(key, 0)
if current >= limit:
return False
cache.set(key, current + 1, timeout=window)
return True
3. Combined middleware example
Integrate verification and rate limiting in middleware so that both checks occur before the view is invoked. Reject requests with invalid signatures or exceeded limits with a 403 response.
from django.utils.deprecation import MiddlewareMixin
class HmacRateLimitMiddleware(MiddlewareMixin):
def process_request(self, request):
if not verify_hmac(request):
raise HttpResponseForbidden('Invalid HMAC')
if not check_rate_limit(request, limit=100, window=60):
raise HttpResponseForbidden('Rate limit exceeded')
4. Include a nonce or request ID when applicable
For endpoints that accept state-changing methods (POST/PUT/PATCH), include a client‑generated nonce or request ID in both the signature and the rate‑limit key to prevent replay within the timestamp window.
# Example client inclusion: X-Nonce: uuid4
# Server side verification
nonce = request.META.get('HTTP_X_NONCE')
if nonce:
canonical += f'|nonce={nonce}'
# Also incorporate into rate_limit_key if you want replay protection to count against limits
By tightly coupling the HMAC signing scope with the rate‑limit key and enforcing strict canonicalization and timestamp checks, you reduce the risk of enumeration and replay abuse while preserving the integrity benefits of HMAC signatures.