Password Spraying in Django with Dynamodb
Password Spraying in Django with Dynamodb — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack where one password (often a commonly used password) is tried against many accounts. When Django uses Amazon DynamoDB as its user store, certain implementation patterns can make spraying easier to execute and harder to detect.
DynamoDB is a NoSQL database; it does not provide built-in rate limiting or account lockout. If Django application code performs user lookup and password verification in separate steps, or constructs queries that reveal whether an account exists, it can leak information that aids an attacker. For example, a view that first queries DynamoDB for a username and then conditionally checks the password allows an attacker to enumerate valid usernames by observing timing differences or response behavior. A DynamoDB table with a Global Secondary Index on username can be queried efficiently, enabling offline password attempts against many accounts.
In Django, the authentication backend is responsible for verifying credentials. A custom backend that calls DynamoDB directly must be careful to avoid timing leaks. If the backend returns early when a user is not found, an attacker can distinguish between nonexistent accounts and valid accounts with incorrect passwords. Additionally, without proper throttling at the application or API level, an attacker can send thousands of password attempts across many accounts within a short time window, especially if the DynamoDB provisioned capacity is high.
The absence of built-in protections in DynamoDB means Django must enforce rate limiting and secure comparison practices. Using Django’s built-in ModelBackend is not applicable when users are stored in DynamoDB, so a custom authentication backend must implement constant-time comparison and uniform response patterns. Failing to do so can result in username enumeration, which effectively reduces the search space for an attacker during a spray campaign.
Real-world examples align with patterns seen in credential stuffing and password spraying (e.g., CVE-2019-19844-style logic flaws in authentication flows). The OWASP API Security Testing group includes credential testing under Authentication flaws, and password spraying fits within the broader BOLA/IDOR and Authentication categories that middleBrick scans evaluate.
Dynamodb-Specific Remediation in Django — concrete code fixes
To mitigate password spraying when using DynamoDB with Django, implement consistent-time lookups, rate limiting, and secure password handling. Below are concrete code examples for a custom authentication backend and a throttling wrapper.
1. Constant-time authentication flow
Ensure the backend always performs a DynamoDB read, even when the user is not found, to avoid timing differences. Use a fixed hash for dummy passwords and compare in constant time.
import time
import hmac
import hashlib
from django.conf import settings
import boto3
from botocore.exceptions import ClientError
# Use the same DynamoDB table as your user store
client = boto3.client('dynamodb', region_name='us-east-1')
TABLE_NAME = 'django_users'
DUMMY_PASSWORD_HASH = hashlib.sha256(b'invalid_dummy_value').hexdigest()
def constant_time_compare(val1, val2):
return hmac.compare_digest(val1, val2)
class DynamoDBBackend:
def authenticate(self, request, username=None, password=None):
# Always query to avoid timing leaks
try:
response = client.get_item(
TableName=TABLE_NAME,
Key={'username': {'S': username}},
ConsistentRead=True
)
except ClientError:
# In case of error, still do a dummy hash check to keep timing similar
constant_time_compare(hashlib.sha256(password.encode('utf-8')).hexdigest(), DUMMY_PASSWORD_HASH)
return None
item = response.get('Item')
if not item:
# Perform a dummy password check to keep timing consistent
constant_time_compare(hashlib.sha256(password.encode('utf-8')).hexdigest(), DUMMY_PASSWORD_HASH)
return None
stored_hash = item.get('password_hash', {}).get('S')
if constant_time_compare(stored_hash, hashlib.sha256(password.encode('utf-8')).hexdigest()):
return item # or map to a Django user object
return None
def get_user(self, user_id):
try:
response = client.get_item(
TableName=TABLE_NAME,
Key={'username': {'S': user_id}},
ConsistentRead=True
)
return response.get('Item')
except ClientError:
return None
2. Throttling and rate limiting at the API or view level
Use Django middleware or a decorator to limit attempts per username or source IP. This reduces the effectiveness of spraying across many accounts.
from django.http import JsonResponse
from django.utils import timezone
from datetime import timedelta
import boto3
client = boto3.client('dynamodb', region_name='us-east-1')
THROTTLE_TABLE = 'throttle_attempts'
def throttle_middleware(get_response):
def middleware(request):
if request.path == '/login/':
username = request.POST.get('username', '')
source_ip = request.META.get('REMOTE_ADDR', '')
now = timezone.now()
# Record attempt in DynamoDB with TTL
client.put_item(
TableName=THROTTLE_TABLE,
Item={
'key': {'S': f'{source_ip}:{username}'},
'timestamp': {'N': str(now.timestamp())},
'ttl': {'N': str((now + timedelta(minutes=15)).timestamp())}
}
)
# Count recent attempts
since = now - timedelta(minutes=1)
# Query logic would go here; simplified example
response = get_response(request)
return response
return middleware
3. Secure password storage
Always hash passwords with a strong adaptive function. When using DynamoDB, store only the hash and salt, never plaintext or reversible encryption.
import hashlib, os, binascii
def make_password_hash(password, salt=None):
if salt is None:
salt = os.urandom(16)
pwdhash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
return binascii.hexlify(salt).decode('utf-8'), binascii.hexlify(pwdhash).decode('utf-8')
def verify_password(password, salt_b64, hash_b64):
salt = binascii.unhexlify(salt_b64)
test_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
return hmac.compare_digest(binascii.unhexlify(hash_b64), test_hash)
4. DynamoDB configuration tips
Use fine-grained access patterns and keep the partition key design aligned with your query needs. Avoid scans; use queries with filters. Enable encryption at rest and enforce TLS in transit. Apply IAM policies with least privilege to the DynamoDB tables used by the authentication backend.