Replay Attack in Django with Bearer Tokens
Replay Attack in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and retransmits it to reproduce the original effect. In Django, using Bearer tokens (typically passed via the Authorization header) without additional protections makes APIs susceptible to this class of issue. Because Bearer tokens are static credentials for the duration of their validity, an intercepted token can be reused by an attacker to impersonate the original caller, even if the underlying transport uses TLS.
When an API endpoint accepts a Bearer token and performs only token validation (e.g., checking signature and scope) without ensuring request uniqueness, replay becomes possible. For example, consider a payment or state-changing endpoint that relies solely on the token for authentication but does not include a nonce, timestamp, or idempotency key. An attacker who captures the Authorization header and the full request can replay the call within the token’s lifetime to perform unauthorized actions, such as initiating duplicate transactions or escalating permissions.
The risk is compounded if the token is long-lived or if the API does not enforce strict transport-layer requirements. Even with HTTPS, a compromised token or a TLS-terminating proxy that logs headers can expose credentials. Moreover, some middleware or load balancers might inadvertently log Authorization headers, increasing the exposure surface. Developers might mistakenly assume that HTTPS alone prevents replay, but without additional mechanisms like one-time nonces or strict timestamp windows, replay remains feasible.
In Django, this often manifests in views or viewsets that check token validity but do not track request identifiers or enforce one-time use. The API may also lack rate limiting at the account or token level, allowing an attacker to submit the same crafted request multiple times. Because middleBrick tests authentication and BOLA/IDOR checks in parallel, it can detect endpoints where token-based authentication is present but replay protections such as unique request identifiers or idempotency are missing.
An example of vulnerable Django REST Framework code that accepts Bearer tokens but does not guard against replay:
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class PaymentView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# Vulnerable: no nonce or idempotency key; same token & payload can be replayed
amount = request.data.get('amount')
# Process payment logic here
return Response({'status': 'processed'})
In this snippet, if an attacker captures the Authorization header (e.g., Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...) and the request body, they can replay the POST to trigger duplicate payments. middleBrick’s checks for Authentication and Property Authorization highlight endpoints where tokens are accepted without additional context to ensure request uniqueness.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
To mitigate replay attacks when using Bearer tokens in Django, implement request-level uniqueness controls such as nonces or idempotency keys, and enforce short token lifetimes where feasible. Combine these with server-side tracking of used identifiers and strict timestamp validation to reject replays.
Use Idempotency Keys: Require clients to send a unique idempotency key (e.g., a UUID) in a header, and store processed keys with a TTL longer than the maximum acceptable replay window. This ensures that repeated identical requests are ignored or return the original response instead of re-executing side effects.
import uuid
from django.utils import timezone
from django.core.cache import cache
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class IdempotentPaymentView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
idempotency_key = request.META.get('HTTP_X_IDEMPOTENCY_KEY')
if not idempotency_key:
return Response({'error': 'X-Idempotency-Key header required'}, status=400)
# Use cache to store key with a TTL (e_response example: 24 hours)
cache_key = f'idempotency:{idempotency_key}'
if cache.get(cache_key):
return Response({'status': 'duplicate request, already processed'})
# Process payment
amount = request.data.get('amount')
# ... actual processing ...
cache.set(cache_key, {'status': 'processed'}, timeout=86400)
return Response({'status': 'processed', 'idempotency_key': idempotency_key})
Validate Timestamps and Reject Stale Requests: Include a timestamp in the request (e.g., an X-Request-Timestamp header) and reject requests where the timestamp falls outside an acceptable window (for example, ±5 minutes). This limits the window during which a captured request can be reused.
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
import time
class TimestampedPaymentView(APIView):
permission_classes = [IsAuthenticated]
ALLOWED_CLOCK_SKEW_SECONDS = 300 # 5 minutes
def post(self, request):
timestamp_header = request.META.get('HTTP_X_REQUEST_TIMESTAMP')
if not timestamp_header:
return Response({'error': 'X-Request-Timestamp header required'}, status=400)
try:
request_timestamp = int(timestamp_header)
except ValueError:
return Response({'error': 'Invalid timestamp'}, status=400)
current_time = int(time.time())
if abs(current_time - request_timestamp) > self.ALLOWED_CLOCK_SKEW_SECONDS:
return Response({'error': 'Request timestamp outside allowed window'}, status=400)
# Proceed with processing
amount = request.data.get('amount')
return Response({'status': 'processed', 'received_at': request_timestamp})
Shorten Token Lifetimes and Use Refresh Flows: Configure token expiration to be as short as practical and encourage clients to use refresh tokens to obtain new access tokens. While this does not directly prevent replay within a single token’s validity, it reduces the impact window. In Django, this can be managed through your token generation and validation logic, ensuring tokens include an exp claim and are verified on each request.
# Example JWT payload with expiration
import jwt
from datetime import datetime, timedelta
payload = {
'sub': 'user-123',
'scope': 'payments:write',
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=15) # short-lived
}
token = jwt.encode(payload, 'your-secret-key', algorithm='HS256')
# Include in Authorization: Bearer {token}
Combine these techniques—idempotency keys, timestamp windows, and short-lived tokens—to significantly reduce the feasibility of replay attacks against Django APIs using Bearer tokens. middleBrick’s scans can validate the presence of these controls by checking Authentication and Property Authorization configurations alongside runtime behavior, helping you prioritize fixes based on actual risk.