HIGH replay attackdjangodynamodb

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 = '[email protected]' 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.

Frequently Asked Questions

Can DynamoDB conditional writes fully prevent replay attacks?
Conditional writes help enforce uniqueness constraints and prevent certain state overwrites, but they do not automatically prevent replay of valid requests. You must pair conditionals with idempotency keys or one-time tokens and ensure clients include nonces to achieve reliable replay protection.
How should I store idempotency keys in DynamoDB for Django?
Create a dedicated DynamoDB table with the request_id as the partition key and a TTL attribute for automatic expiration. Use a ConditionExpression of attribute_not_exists(request_id) on writes so that replays fail safely, and design the TTL to exceed your maximum acceptable replay window.