Crlf Injection in Flask with Dynamodb
Crlf Injection in Flask with Dynamodb — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when user-controlled data is reflected in HTTP headers without proper sanitization, allowing an attacker to inject CRLF sequences (e.g., \r\n) to split headers and inject new ones. In a Flask application that interacts with DynamoDB, this typically arises when values stored in or retrieved from DynamoDB are used in HTTP response headers, such as custom headers, Location, or Set-Cookie.
Flask routes often construct headers using values directly from request parameters or from DynamoDB items. If a DynamoDB attribute (e.g., a user-supplied tag, display name, or redirect target) contains \r\n sequences and is placed into a header, Flask (via Werkzeug) may allow header injection. This can enable HTTP response splitting, cache poisoning, or cross-site scripting via injected headers. DynamoDB itself does not introduce the flaw, but its use as a data source for headers creates a path for injection when input validation is missing.
Consider a Flask route that reads a user profile from DynamoDB and sets a custom X-Display-Name header. If the profile attribute displayName contains \r\n, an attacker can inject additional headers:
GET /profile?user_id=123 HTTP/1.1
Host: example.com
If the backend does not sanitize displayName, the response might include:
HTTP/1.1 200 OK
X-Display-Name: Alice\r\nSet-Cookie: session=attacker
Content-Type: text/html
In this scenario, the DynamoDB-stored value directly enables header injection. Additionally, if the Flask app uses DynamoDB data to construct a redirect (e.g., a stored URL or location), unsanitized newlines can manipulate the Location header, leading to open redirects or header smuggling. Because DynamoDB stores and returns raw strings, the onus is on the Flask application to validate and sanitize any data used in headers, regardless of the database.
Dynamodb-Specific Remediation in Flask — concrete code fixes
To prevent Crlf Injection when using DynamoDB data in Flask headers, enforce strict input validation and header-safe encoding. Avoid directly inserting user-derived or database-derived strings into HTTP headers. Instead, sanitize newlines and carriage returns, and prefer safe representations. Below are concrete patterns and DynamoDB code examples for Flask.
1. Validate and sanitize header inputs
Create a validation utility that rejects or strips CR and LF characters from any value destined for headers. If the field is required, reject the input early with a clear error.
import re
def sanitize_header_value(value: str) -> str:
# Reject if contains CR or LF
if re.search(r'[\r\n]', value):
raise ValueError('Header value contains disallowed characters')
return value
2. Use DynamoDB DocumentClient with explicit type handling
When retrieving items from DynamoDB, explicitly handle types and apply sanitization before using values in headers. This example uses boto3 with DocumentClient to get a user item and safely set a header.
import boto3
from flask import Flask, request, make_response
app = Flask(__name__)
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
table = dynamodb.Table('Users')
@app.route('/profile')
def profile():
user_id = request.args.get('user_id', '')
response = make_response({'message': 'ok'})
# Fetch item from DynamoDB
result = table.get_item(Key={'user_id': user_id})
item = result.get('Item')
if item and 'display_name' in item:
# Sanitize before using in header
display_name = item['display_name']
if re.search(r'[\r\n]', display_name):
response.headers['X-Display-Name'] = 'Invalid'
else:
response.headers['X-Display-Name'] = display_name
return response
3. Encode or omit problematic fields
If you must retain newline-containing data, either omit the header or encode the value (e.g., base64) to prevent injection. Never use raw newlines in headers.
import base64
if item and 'display_name' in item:
raw = item['display_name']
if re.search(r'[\r\n]', raw):
# Safe fallback: encode and indicate encoding
encoded = base64.b64encode(raw.encode('utf-8')).decode('ascii')
response.headers['X-Display-Name'] = f'base64:{encoded}'
else:
response.headers['X-Display-Name'] = raw
4. Apply framework-level protections
Flask does not automatically sanitize headers. Use a request preprocessor or a before_request hook to validate known parameters that may originate from DynamoDB, especially if they are used across multiple endpoints.
@app.before_request
def validate_inputs():
# Example: ensure no dangerous characters in query params used for headers
for key in request.args:
val = request.args.get(key, '')
if re.search(r'[\r\n]', val):
# Reject or sanitize early
raise ValueError(f'Disallowed characters in {key}')
5. Secure redirect handling with DynamoDB data
If using DynamoDB to store redirect targets, resolve and sanitize before issuing a redirect. Avoid Location header injection by validating URLs and stripping newlines.
from urllib.parse import urlparse
@app.route('/redirect')
def redirect_user():
target = item.get('redirect_url', '') if item else ''
# Basic validation: ensure no newlines and a safe scheme
if re.search(r'[\r\n]', target) or not urlparse(target).scheme in ('https',):
return {'error': 'invalid redirect'}, 400
response = make_response()
response.headers['Location'] = target
response.status_code = 303
return response