# CRLF Injection via Untrusted HTTP Response Headers

Language: Python
Severity: High
CWE: CWE-93

## Source
11, 14

## Flow
11-22, 14-23

## Sink
22, 23

## Vulnerable Code
```python
from flask import Flask, request, Response
import boto3

app = Flask(__name__)
s3_client = boto3.client('s3')

@app.route('/cloud/presigned-download')
def generate_presigned_url():
    bucket_name = request.args.get('bucket', 'default-storage')
    object_key = request.args.get('key')
    user_agent = request.headers.get('User-Agent', 'unknown')
    
    presigned_url = s3_client.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket_name, 'Key': object_key},
        ExpiresIn=3600
    )
    
    download_filename = request.args.get('filename', 'download.bin')
    
    resp = Response(presigned_url)
    resp.headers['X-Download-Filename'] = download_filename
    resp.headers['X-Client-Agent'] = user_agent
    resp.headers['Content-Type'] = 'text/plain'
    
    return resp
```

## Explanation

The application directly incorporates user-controlled input from the 'filename' query parameter and 'User-Agent' header into HTTP response headers without sanitization. An attacker can inject CRLF characters (\r\n) to inject arbitrary HTTP headers or response body content, potentially leading to HTTP response splitting, cache poisoning, or cross-site scripting attacks.

## Remediation

The fix introduces two sanitization functions: sanitize_header_value() strips all CRLF and control characters from header values, and sanitize_filename() applies a strict allowlist of safe characters for filenames. Both user-controlled inputs (the 'filename' query parameter and 'User-Agent' header) are sanitized before being set as response headers, preventing CRLF injection and HTTP response splitting attacks.

## Secure Code
```python
from flask import Flask, request, Response
import boto3
import re

app = Flask(__name__)
s3_client = boto3.client('s3')

def sanitize_header_value(value):
    """Remove CRLF characters and other control characters from header values."""
    if value is None:
        return ''
    # Remove carriage return, line feed, and other control characters
    sanitized = re.sub(r'[\r\n\x00-\x1f\x7f]', '', value)
    return sanitized

def sanitize_filename(filename):
    """Sanitize filename for use in headers, allowing only safe characters."""
    if filename is None:
        return 'download.bin'
    # Remove any characters that are not alphanumeric, dots, hyphens, or underscores
    sanitized = re.sub(r'[^a-zA-Z0-9._\-]', '_', filename)
    # Prevent empty filename
    if not sanitized:
        return 'download.bin'
    return sanitized

@app.route('/cloud/presigned-download')
def generate_presigned_url():
    bucket_name = request.args.get('bucket', 'default-storage')
    object_key = request.args.get('key')
    user_agent = request.headers.get('User-Agent', 'unknown')
    
    if not object_key:
        return Response('Missing required parameter: key', status=400)
    
    presigned_url = s3_client.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket_name, 'Key': object_key},
        ExpiresIn=3600
    )
    
    download_filename = request.args.get('filename', 'download.bin')
    
    # Sanitize user-controlled values before setting them as headers
    safe_filename = sanitize_filename(download_filename)
    safe_user_agent = sanitize_header_value(user_agent)
    
    resp = Response(presigned_url)
    resp.headers['X-Download-Filename'] = safe_filename
    resp.headers['X-Client-Agent'] = safe_user_agent
    resp.headers['Content-Type'] = 'text/plain'
    
    return resp
```
