Header Injection in Django with Dynamodb
Header Injection in Django with Dynamodb — how this specific combination creates or exposes the vulnerability
Header Injection occurs when untrusted input is reflected into HTTP response headers without validation or encoding. In a Django application that uses Amazon DynamoDB as a backend, the risk pattern typically arises when developer-controlled data—such as a value read from a DynamoDB item—is placed into a response header, for example via HttpResponse or a streaming response. Because HTTP headers have a strict format, newlines (CRLF, \r\n) in injected content can break header structure and enable header splitting, allowing an attacker to inject additional headers, set malicious cookies, or perform HTTP response splitting.
With DynamoDB, the exposure path often begins with how data is retrieved and used. A common pattern is querying a DynamoDB table for user or configuration data and then directly using an attribute in a header. For instance, if a table stores custom metadata such as X-Custom-Reason or display names, and those attributes are forwarded into headers without sanitization, an attacker who can influence the DynamoDB content (for example, through registration or admin interfaces) can inject CRLF sequences. In Django, using boto3 to fetch items does not inherently sanitize data; it returns raw attribute values that may contain newline characters. When these values are used in headers, the resulting response can be manipulated to include extra headers like Set-Cookie or to split the response body, leading to HTTP response splitting (OWASP API Top 10:2023 — Injection). This becomes a concrete concern when DynamoDB is used to store or reference data that ultimately appears in headers, and when Django does not enforce strict allowlists on header values.
The combination amplifies impact because DynamoDB’s schema flexibility means unexpected or maliciously crafted strings can be stored and later retrieved, and Django’s default behavior of passing such values directly into headers does not enforce safe serialization. For example, an attacker who registers with a username containing a newline may not immediately exploit it, but if that username is later read from DynamoDB and placed into a X-Username header, the injected newline can split the header chain. This is especially relevant when responses include custom headers derived from DynamoDB, or when debugging or tracing headers are constructed from item attributes. The unauthenticated attack surface tested by scanners like middleBrick includes such indirect injection paths where data storage (DynamoDB) and presentation (Django response headers) are weakly coupled in terms of validation.
Dynamodb-Specific Remediation in Django — concrete code fixes
Remediation focuses on preventing untrusted data from reaching HTTP headers and, when headers must include dynamic values, enforcing strict sanitization and encoding. The most robust approach is to avoid putting DynamoDB-derived values into headers entirely; if necessary, apply an allowlist filter that removes or replaces CRLF characters and other control characters. Below are concrete, DynamoDB-aware patterns for Django that reduce risk.
1. Validate and sanitize DynamoDB-derived header values
When you must use an attribute from a DynamoDB item in a header, sanitize it by removing or replacing newline and carriage return characters. Prefer an allowlist approach for header values.
import re
import boto3
from django.http import HttpResponse
def get_user_header(user_id: str) -> HttpResponse:
client = boto3.client('dynamodb', region_name='us-east-1')
response = client.get_item(
TableName='UserMetadata',
Key={'user_id': {'S': user_id}}
)
item = response.get('Item', {})
# Safe extraction with fallback
raw_value = item.get('display_name', {}).get('S', '')
# Remove CRLF to prevent header injection
safe_value = re.sub(r'[\r\n]', '', raw_value)
response = HttpResponse(content='OK')
response['X-Display-Name'] = safe_value
return response
2. Use a strict allowlist for known-safe values
Instead of removing characters, validate that the value conforms to an expected pattern (e.g., alphanumeric and limited punctuation) before using it in a header.
import re
import boto3
from django.http import HttpResponse
NAME_PATTERN = re.compile(r'^[A-Za-z0-9 _\-\.]+$')
def get_safe_header_response(user_id: str) -> HttpResponse:
client = boto3.client('dynamodb', region_name='us-east-1')
resp = client.get_item(TableName='Users', Key={'id': {'S': user_id}})
item = resp.get('Item', {})
name_attr = item.get('name', {}).get('S', '')
if not NAME_PATTERN.fullmatch(name_attr):
# Reject or default to a safe value
name_attr = 'unknown'
response = HttpResponse(content='OK')
response['X-User-Name'] = name_attr
return response
3. Leverage Django utilities for header encoding
For headers like Content-Disposition where encoding is standardized, use Django’s built-in utilities or RFC-compliant formatting rather than string concatenation with raw DynamoDB values.
from django.http import HttpResponse
import boto3
from urllib.parse import quote
def attachment_response(user_id: str) -> HttpResponse:
client = boto3.client('dynamodb', region_name='us-east-1')
item = client.get_item(TableName='Files', Key={'id': {'S': user_id}})
filename = item.get('Item', {}).get('filename', {}).get('S', 'file')
# Encode per RFC 5987; avoids injection via special characters
encoded = quote(filename, safe='')
response = HttpResponse(content=b'', content_type='application/octet-stream')
response['Content-Disposition'] = f'attachment; filename*=utf-8\'\'{encoded}'
return response
4. Centralize header construction and reject unsafe sources
Create a small utility that ensures any header value derived from external stores (including DynamoDB) passes through a normalization step that strips or rejects control characters.
def safe_header_value(value: str) -> str:
# Strip leading/trailing whitespace and remove CRLF
return value.strip().replace('\r', '').replace('\n', '')
# Usage with boto3-fetched data
import boto3
from django.http import HttpResponse
client = boto3.client('dynamodb', region_name='us-east-1')
item = client.get_item(TableName='Config', Key={'key': {'S': 'branding'}})['Item']
custom_header = safe_header_value(item.get('brand_name', {}).get('S', ''))
response = HttpResponse(content='OK')
response['X-Brand'] = custom_header