Replay Attack in Django with Dynamodb
Replay Attack in Django with Dynamodb — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an adversary intercepts a valid request or token and retransmits it to reproduce the original effect. In a Django application that uses Amazon DynamoDB as its primary data store, the combination of Django’s session and authentication patterns with DynamoDB’s eventual-consistency characteristics and idempotency-sensitive operations can inadvertently enable replay-based threats.
Consider a typical Django view that writes sensitive state changes to DynamoDB, such as updating a user’s email or initiating a money transfer. If the request lacks a unique, single-use identifier and the view relies solely on HTTP method and endpoint routing without server-side nonce or timestamp validation, an attacker who captures a signed request (e.g., via a compromised network or client-side storage) can replay it. Because DynamoDB does not provide built-in request-level deduplication, the same write operation may be applied multiple times if the application does not enforce idempotency keys or conditional writes.
DynamoDB’s conditional writes (using ConditionExpression) can mitigate some forms of state corruption, but they do not inherently prevent a replay that carries a valid condition. For example, a request to set email = 'new@example.com' with a condition on the current email value could be replayed with the same condition and succeed on the second attempt if the first replay updates the attribute. Additionally, session data stored in DynamoDB (e.g., using a custom session backend) may include timestamps or tokens; if these are not bound to a per-request nonce or a short-lived, single-use token, intercepted session cookies or API tokens can be reused.
Common vulnerable patterns include:
- Using unsigned, replayable HTTP requests to DynamoDB-backed endpoints without a request ID or timestamp nonce.
- Storing one-time actions (such as password resets) as plain records in DynamoDB without a uniqueness constraint or a consumed flag.
- Relying solely on HTTPS for confidentiality without applying anti-replay protections at the application layer, allowing captured TLS-protected payloads to be reused against idempotent endpoints.
These risks are particularly relevant when DynamoDB streams or time-to-live (TTL) are used for event-driven processing, as replayed events may trigger unintended side effects after their original validity window has closed.
Dynamodb-Specific Remediation in Django — concrete code fixes
To defend against replay attacks in a Django application that stores data in DynamoDB, combine Django-side controls with DynamoDB features such as conditional writes, unique identifiers, and idempotency keys. Below are concrete, executable patterns.
1. Idempotency key with DynamoDB conditional write
Require clients to provide an idempotency key (e.g., a UUID) for state-changing operations. Store processed keys in DynamoDB with a TTL so that replays are ignored after a safe window.
import boto3
from django.conf import settings
dynamodb = boto3.resource('dynamodb', region_name=settings.AWS_REGION)
table = dynamodb.Table(settings.DYNAMODB_IDEMPOTENCY_TABLE)
def process_idempotent_request(request_id, user_id, new_email):
# Try to record this request_id atomically
response = table.put_item(
Item={
'request_id': request_id,
'user_id': user_id,
'email': new_email,
'ttl': int(time.time()) + 86400 # 24h TTL
},
ConditionExpression='attribute_not_exists(request_id)'
)
# If ConditionExpression fails, the request was already processed
return response
2. One-time token for password reset stored in DynamoDB
Issue a single-use token with an expiry, store it in DynamoDB with a consumed flag, and ensure validation consumes it on first use.
import time
import uuid
def create_password_reset_token(user_id):
token = str(uuid.uuid4())
ttl = int(time.time()) + 900 # 15 minutes
table = dynamodb.Table('password_reset_tokens')
table.put_item(Item={
'user_id': user_id,
'token': token,
'consumed': False,
'ttl': ttl
})
return token
def consume_reset_token(token):
table = dynamodb.Table('password_reset_tokens')
# Conditional update to mark as consumed only if not already consumed
response = table.update_item(
Key={'token': token},
UpdateExpression='SET consumed = :val',
ConditionExpression='consumed = :false',
ExpressionAttributeValues={':val': True, ':false': False},
ReturnValues='UPDATED_NEW'
)
return response
3. Per-request nonce in Django views with DynamoDB verification
Generate and store a nonce per session or per request, and require it on submission. Validate against DynamoDB to ensure freshness and single use.
import secrets
from datetime import datetime, timedelta
def generate_and_store_nonce(user_id):
nonce = secrets.token_urlsafe(16)
table = dynamodb.Table('nonces')
table.put_item(Item={
'user_id': user_id,
'nonce': nonce,
'expires_at': datetime.utcnow() + timedelta(minutes=5)
})
return nonce
def validate_nonce(user_id, nonce):
table = dynamodb.Table('nonces')
response = table.get_item(Key={'user_id': user_id, 'nonce': nonce})
item = response.get('Item')
if item and item['expires_at'] > datetime.utcnow():
# Consume the nonce to prevent reuse
table.delete_item(Key={'user_id': user_id, 'nonce': nonce})
return True
return False
4. Use DynamoDB Streams and deduplication window
For event-driven processing, maintain a small deduplication cache (e.g., in Redis or a DynamoDB TTL table) of recently seen message IDs from DynamoDB Streams to discard duplicates before applying business logic.
5. Enforce short-lived, signed URLs and tokens
When exposing endpoints that write to DynamoDB, use Django’s signed cookies or JWTs with short expirations, and bind them to a per-request nonce or timestamp to limit replay windows.
By combining these patterns—unique request identifiers, conditional writes, one-time tokens with atomic consume, and short-lived nonces—you significantly reduce the attack surface for replay attacks in a Django + DynamoDB architecture.