Dictionary Attack in Django with Bearer Tokens
Dictionary Attack in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A dictionary attack in Django involving Bearer tokens typically targets an endpoint that accepts token-based authentication but lacks sufficient protections against token guessing or token enumeration. When an API uses Bearer tokens for authentication, each request includes an Authorization: Bearer <token> header. If token generation is predictable, tokens are leaked via logs or error messages, or endpoints do not enforce rate limiting, an attacker can systematically submit likely token values to discover valid tokens and gain unauthorized access.
Consider an endpoint that returns user profile data when provided with a valid Bearer token but does not enforce authentication or rate controls:
from django.http import JsonResponse
from django.views import View
class ProfileView(View):
def get(self, request):
auth = request.headers.get("Authorization")
if auth and auth.startswith("Bearer "):
token = auth.split(" ")[1]
# Insecure: no validation, rate limiting, or token lookup
if validate_token_simple(token):
return JsonResponse({"username": "alice", "role": "user"})
return JsonResponse({"error": "Unauthorized"}, status=401)
If validate_token_simple performs a naive lookup or the token space is small (e.g., short numeric tokens), an attacker can iterate through likely tokens — a dictionary attack — to discover valid tokens. This becomes more feasible when token generation lacks sufficient entropy, when tokens are derived from user attributes, or when token leakage occurs via referrer headers, logs, or client-side storage.
Django REST framework (DRF) APIs that use token authentication can also be vulnerable if token creation is weak or if the token endpoint does not implement throttling:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
class SecureResourceView(APIView):
def get(self, request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response({"detail": "Authentication credentials were not provided."}, status=401)
token_key = auth_header.split(" ")[1]
try:
token = Token.objects.get(key=token_key)
return Response({"data": "Sensitive resource"})
except Token.DoesNotExist:
return Response({"detail": "Invalid token."}, status=401)
Without rate limiting, an attacker can perform a dictionary attack by submitting many token guesses. If token keys are generated using a predictable pattern (e.g., sequential integers or low-entropy strings), the attack becomes practical. Additionally, if responses differ meaningfully between valid and invalid tokens (e.g., returning user-specific data vs a generic error), the API leaks information that facilitates token discovery.
Django settings can inadvertently expose tokens through insecure configurations. For example, logging request headers without redaction may write Bearer tokens to application logs:
LOGGING = {
"version": 1,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {
"django.request": {
"handlers": ["console"],
"level": "WARNING",
"propagate": True,
}
}
}
If tokens appear in the Authorization header and are logged in full, attackers who gain access to logs can directly use those Bearer tokens. This transforms a theoretical dictionary attack into a practical credential compromise scenario.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
To secure Django APIs using Bearer tokens, enforce strong token generation, require HTTPS, implement rate limiting, normalize error responses, and avoid logging sensitive headers. Below are concrete fixes and code examples.
1. Use cryptographically random tokens
Ensure token generation uses a cryptographically secure random source with sufficient length. If using Django REST framework authtoken, generate tokens with sufficient entropy:
import secrets
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
for user in User.objects.all():
# Replace existing token with a high-entropy token
token, _ = Token.objects.get_or_create(user=user)
if not token.key or len(token.key) < 32:
token.key = secrets.token_urlsafe(32)
token.save()
2. Require HTTPS and secure cookies
Ensure your Django settings enforce HTTPS in production to prevent token interception:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
3. Add rate limiting to token validation
Use Django middleware or DRF throttling to limit requests per IP or token to mitigate dictionary attacks:
from rest_framework.throttling import AnonRateThrottle
class BearerTokenThrottle(AnonRateThrottle):
rate = '100/hour' # adjust to your risk tolerance
def get_cache_key(self, request, view=None):
auth = request.headers.get("Authorization")
if auth and auth.startswith("Bearer "):
return auth.split(" ")[1] # rate by token
return request.META.get("REMOTE_ADDR")
Apply the throttle globally or to sensitive views:
from rest_framework.views import APIView
from rest_framework.response import Response
from .throttles import BearerTokenThrottle
class SecureResourceView(APIView):
throttle_classes = [BearerTokenThrottle]
def get(self, request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response({"detail": "Authentication credentials were not provided."}, status=401)
token_key = auth_header.split(" ")[1]
# Use a constant-time comparison and avoid leaking token validity
try:
token = Token.objects.get(key=token_key)
return Response({"data": "Sensitive resource"})
except Token.DoesNotExist:
# Always return the same generic response
return Response({"detail": "Invalid credentials."}, status=401)
4. Avoid logging Bearer tokens
Customize logging to redact Authorization headers:
import logging
from django.utils.log import AdminEmailHandler
class RedactingFilter(logging.Filter):
def filter(self, record):
if hasattr(record, "msg"):
# Simple redaction for demonstration; tailor to your logging setup
if isinstance(record.msg, str):
record.msg = record.msg.replace(record.args.get('auth', ''), '[REDACTED]') if 'auth' in record.args else record.msg
return True
logger = logging.getLogger("django.request")
for handler in logger.handlers:
handler.addFilter(RedactingFilter())
5. Use a secure token format and constant-time comparison
Always compare tokens using a constant-time function to prevent timing attacks:
import hmac
from django.conf import settings
def safe_token_compare(token_a, token_b):
return hmac.compare_digest(token_a, token_b)
# In your view:
# if safe_token_compare(token_key, known_token):
6. Implement token revocation and short lifetimes
For high-security scenarios, pair Bearer tokens with short expiration times and a revocation mechanism. If you issue JWTs, validate signatures and enforce exp/nbf claims; otherwise maintain a denylist for invalidated tokens.
By combining strong token generation, HTTPS enforcement, rate limiting, safe error handling, and secure logging, you significantly reduce the feasibility of dictionary attacks against Django APIs using Bearer tokens.