Prototype Pollution in Django with Dynamodb
Prototype Pollution in Django with Dynamodb — how this specific combination creates or exposes the vulnerability
Prototype pollution in a Django application using Amazon DynamoDB as the backend can occur when user-controlled input is merged into application objects that are later serialized into DynamoDB item structures. In JavaScript-based object handling this is a well-known class of vulnerability, but in Python/Django the same conceptual risk arises when dictionaries that represent item attributes are constructed from request data without strict validation. If a view builds an item dictionary by updating a base schema with values parsed from query parameters, JSON payloads, or form data, an attacker can inject keys such as __proto__, constructor, or other properties that affect object behavior in the application layer before the data is written to DynamoDB.
Consider a Django view that accepts JSON to update a DynamoDB item. If the view does not deeply validate or whitelist fields, an attacker can send a payload like {"price": 100, "__proto__": {"isAdmin": true}}. Python’s dict update mechanisms may propagate these keys into shared templates or class-like dictionaries used across the request, effectively polluting the logical prototype chain used by the application. When the polluted dictionary is marshaled into a DynamoDB PutItem or UpdateItem request, the malicious keys can be stored as attribute names, leading to unexpected behavior when the item is read and interpreted by downstream consumers, such as other services that rely on schema integrity.
DynamoDB itself does not execute code or interpret object prototypes, but the vulnerability manifests in how Django constructs and uses item dictionaries before sending them to the database. For example, if the application uses a utility that flattens or merges dictionaries for conditional update expressions (e.g., using UpdateExpression with placeholder values derived from user input), injected keys can alter the expression syntax or cause unintended attribute updates. This can lead to privilege escalation (e.g., modifying an is_admin attribute), data integrity issues, or bypass of business logic checks enforced in Python code rather than at the database level.
An example of unsafe construction in Django with DynamoDB:
import boto3
from django.http import JsonResponse
def update_item(request, item_id):
data = request.POST.dict() # Unsafe: directly uses user input
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Items')
# Unsafe merge: attacker can inject keys like '__proto__'
item = {'id': item_id, **data}
table.put_item(Item=item)
return JsonResponse({'status': 'updated'})
In this pattern, data is merged into item without validation. If an attacker controls a parameter named __proto__, it becomes a key in the DynamoDB item, polluting the application’s logical object model. Downstream processing that relies on Python object inheritance or shared dictionaries may behave unexpectedly, enabling security-relevant changes.
Dynamodb-Specific Remediation in Django — concrete code fixes
To protect Django applications using DynamoDB, enforce strict input validation and avoid merging raw user input into item dictionaries. Use schema definitions and allowlists to ensure only expected attributes are accepted, and serialize data with care to prevent unintended key propagation.
1. Validate and whitelist fields before constructing DynamoDB items. Define an allowed set of attributes and map known types explicitly.
from django.http import JsonResponse
import boto3
ALLOWED_FIELDS = {'name', 'price', 'category'}
def safe_update_item(request, item_id):
allowed = ALLOWED_FIELDS
raw = request.POST.dict()
# Build item using only allowed keys with type checks
item = {'id': item_id}
for key, value in raw.items():
if key in allowed:
if key == 'price':
item[key] = float(value)
else:
item[key] = value
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Items')
table.put_item(Item=item)
return JsonResponse({'status': 'updated'})
2. Use DynamoDB expression attribute values instead of merging dictionaries to avoid injection via key names. This keeps user data in values, not in attribute names.
import boto3
def update_with_expressions(request, item_id):
data = request.POST.dict()
# Only permit known safe attributes
valid_updates = {k: v for k, v in data.items() if k in {'price', 'category'}}
if not valid_updates:
return JsonResponse({'error': 'no valid fields'}, status=400)
set_clauses = [f'#{k} = :{k}' for k in valid_updates.keys()]
expression = f'UPDATE table SET {', '.join(set_clauses)} WHERE id = :id'
expression_attribute_names = {f'#{k}': k for k in valid_updates.keys()}
expression_attribute_values = {f':{k}': v for k, v in valid_updates.items()}
expression_attribute_values[':id'] = item_id
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Items')
table.update_item(
Key={'id': item_id},
UpdateExpression=expression,
ExpressionAttributeNames=expression_attribute_names,
ExpressionAttributeValues=expression_attribute_values
)
return JsonResponse({'status': 'updated'})
3. Sanitize keys before any dictionary merging or serialization. Reject or rename keys that could pollute object prototypes or application semantics.
def sanitize_keys(payload):
dangerous = {'__proto__', 'constructor', 'prototype'}
return {k: v for k, v in payload.items() if k not in dangerous}
def update_with_sanitized(request, item_id):
raw = sanitize_keys(request.POST.dict())
item = {'id': item_id, **raw}
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Items')
table.put_item(Item=item)
return JsonResponse({'status': 'updated'})
These approaches ensure that user input never directly dictates attribute names in DynamoDB item structures, preventing prototype pollution-style attacks while maintaining compatibility with DynamoDB’s attribute-value model.