Credential Stuffing in Django with Hmac Signatures
Credential Stuffing in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack where lists of breached username and password pairs are used to gain unauthorized access to user accounts. In Django, developers sometimes add HMAC-based signatures to request payloads or headers to prove request origin or to sign a token without necessarily enforcing strict authentication or rate controls. When HMAC signatures are used without additional protections, they can inadvertently expose or amplify credential stuffing risks.
Consider a scenario where a Django endpoint accepts a signature in a header to authorize an action without traditional session cookies or token introspection. If the endpoint relies only on signature validity and does not couple it with per-request nonces, strict origin checks, or rate limits, an attacker can replay captured signed requests at scale. Because the signature is valid and the endpoint treats it as proof of legitimacy, the attacker can iterate through credential pairs, submitting signed login or password-reset requests without triggering suspicion.
The vulnerability is not in HMAC itself—a cryptographic primitive for integrity—but in how the application uses it. For example, if the signing key is static and exposed, or if the signed payload includes a predictable user identifier without replay protection, credential stuffing scripts can reuse or slightly mutate signed requests. This is especially risky when the endpoint does not enforce rate limiting per user or per IP, allowing rapid, automated attempts that bypass protections that would otherwise block credential stuffing.
Another angle is authentication bypass via signed tokens that lack revocation or strict scope. If a Django API uses HMAC signatures to identify a user ID in a token and does not validate the token’s freshness or binding to a session, an attacker with a leaked signature can attempt to sign in with different credentials while the signed payload remains valid. Since the signature verifies integrity but not current authorization, the application may treat the request as authenticated, enabling account takeover through credential stuffing techniques combined with replayed or slightly modified signed requests.
Real-world attack patterns such as OWASP API Top 10:2023 Broken Object Level Authorization (BOLA) and insecure direct object references (IDOR) intersect with this risk when signed endpoints expose user-specific operations without additional checks. For example, a signed endpoint that changes a user’s email or password must ensure the requester is the legitimate user, not an automated script replaying a previously captured, valid signature. Without coupling HMAC integrity checks with strong authentication, rate limiting, and anti-replay mechanisms, the combination of Django, HMAC signatures, and credential stuffing creates a pathway for unauthorized access.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate credential stuffing risks when using HMAC signatures in Django, you should enforce replay protection, strict origin validation, and rate limiting, and ensure signatures are bound to a specific scope and short lifetime. Below are concrete code examples that demonstrate secure practices.
1. Signed request with nonce and timestamp
Include a nonce and timestamp in the signed payload, and validate them on each request to prevent replay.
import hmac
import hashlib
import time
import secrets
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
SECRET_KEY = b'your-secure-secret-key' # store in settings, use secrets or env
NONCE_STORE = set() # use Redis in production
MAX_AGE = 30 # seconds
def verify_signature(data, received_signature):
message = f"{data['timestamp']}{data['nonce']}{data['user_id']}".encode()
expected = hmac.new(SECRET_KEY, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)
@csrf_exempt
@require_POST
def protected_action(request):
import json
try:
payload = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'invalid_json'}, status=400)
timestamp = payload.get('timestamp')
nonce = payload.get('nonce')
user_id = payload.get('user_id')
signature = request.headers.get('X-Signature')
if not all([timestamp, nonce, user_id, signature]):
return JsonResponse({'error': 'missing_fields'}, status=400)
# Replay protection
if nonce in NONCE_STORE:
return JsonResponse({'error': 'replay_detected'}, status=403)
NONCE_STORE.add(nonce)
# Freshness check
if abs(time.time() - int(timestamp)) > MAX_AGE:
return JsonResponse({'error': 'stale_request'}, status=403)
data = {'timestamp': timestamp, 'nonce': nonce, 'user_id': user_id}
if not verify_signature(data, signature):
return JsonResponse({'error': 'invalid_signature'}, status=403)
# At this point, the signed request is valid and replay-free
# Proceed with business logic, ensuring the user_id matches the intended resource
return JsonResponse({'status': 'ok', 'user_id': user_id})
2. Per-user rate limiting and scope binding
Bind the signature to an action scope and enforce per-user rate limits to reduce the effectiveness of credential stuffing.
from django.core.cache import cache
from django.http import JsonResponse
from django.views.decorators.http import require_POST
import hmac, hashlib, json, time
RATE_LIMIT = 5 # requests
RATE_WINDOW = 60 # seconds
def check_rate_limit(user_id):
key = f"rl:{user_id}"
current = cache.get(key, 0)
if current >= RATE_LIMIT:
return False
cache.incr(key)
if current == 0:
cache.expire(key, RATE_WINDOW)
return True
@require_POST
def scoped_action(request):
body = json.loads(request.body)
user_id = body.get('user_id')
action = body.get('action')
signature = request.headers.get('X-Signature')
# Scope the signature to the action and user
message = f"{user_id}:{action}:{int(time.time())}".encode()
expected = hmac.new(SECRET_KEY, message, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return JsonResponse({'error': 'invalid_scope_or_signature'}, status=403)
if not check_rate_limit(user_id):
return JsonResponse({'error': 'rate_limit_exceeded'}, status=429)
# Proceed with action, ensuring user owns the resource
return JsonResponse({'status': 'processed', 'action': action, 'user_id': user_id})
3. Using Django packages and secure key management
Leverage well‑maintained libraries and avoid hard‑coded secrets. Use environment variables and rotate keys periodically.
# settings.py
import os
SECRET_KEY = os.environ.get('DJANGO_HMAC_SECRET')
# views.py
from django.conf import settings
import hmac, hashlib
def create_signed_token(user_id, extra=''):
import time
payload = f"{user_id}|{int(time.time())}|{extra}"
return hmac.new(
settings.SECRET_KEY.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
These patterns emphasize that HMAC signatures provide integrity and origin authentication but must be combined with replay protection (nonces/timestamps), scope binding, and rate limiting to be effective against credential stuffing. Do not rely on signatures alone; couple them with strong authentication controls and monitoring.