# HTTP Response Splitting via Unvalidated Redirect Location Header

Language: Python
Severity: High
CWE: CWE-113

## Source
8, 9

## Flow
8-12, 9-13

## Sink
12, 13

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

app = Flask(__name__)

@app.route('/iot/device/provision')
def provision_device():
    device_id = request.args.get('device_id', '')
    callback_endpoint = request.args.get('callback', '/dashboard')
    logging.info(f'Provisioning device: {device_id}')
    response = Response('Device provisioned successfully', status=302)
    response.headers['Location'] = callback_endpoint
    response.headers['X-Device-ID'] = device_id
    return response
```

## Explanation

The application accepts untrusted input from request parameters (device_id and callback) and directly injects them into HTTP response headers without validation or sanitization. An attacker can inject CRLF characters (\r\n) to split the HTTP response, inject arbitrary headers, or perform HTTP Response Splitting attacks leading to XSS, cache poisoning, or session hijacking.

## Remediation

The fix sanitizes both user-controlled inputs before placing them in HTTP headers. CRLF and control characters are stripped from all header values, the redirect URL is validated against an allowlist of safe relative paths to prevent both response splitting and open redirects, and the device_id is validated to only contain alphanumeric characters, hyphens, and underscores.

## Secure Code
```python
from flask import Flask, request, Response, abort
import logging
import re
from urllib.parse import urlparse

app = Flask(__name__)

ALLOWED_REDIRECT_PATHS = ['/dashboard', '/devices', '/config', '/status']

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

def validate_redirect_url(url):
    """Validate that the redirect URL is a safe relative path."""
    sanitized = sanitize_header_value(url)
    parsed = urlparse(sanitized)
    if parsed.scheme or parsed.netloc:
        return '/dashboard'
    if not sanitized.startswith('/'):
        return '/dashboard'
    path_base = sanitized.split('?')[0]
    if path_base not in ALLOWED_REDIRECT_PATHS:
        return '/dashboard'
    return sanitized

def validate_device_id(device_id):
    """Validate device_id contains only safe characters."""
    if not device_id:
        return ''
    if not re.match(r'^[a-zA-Z0-9_\-]+$', device_id):
        abort(400, description='Invalid device_id format')
    return device_id

@app.route('/iot/device/provision')
def provision_device():
    device_id = request.args.get('device_id', '')
    callback_endpoint = request.args.get('callback', '/dashboard')
    device_id = validate_device_id(device_id)
    safe_callback = validate_redirect_url(callback_endpoint)
    logging.info(f'Provisioning device: {device_id}')
    response = Response('Device provisioned successfully', status=302)
    response.headers['Location'] = safe_callback
    if device_id:
        response.headers['X-Device-ID'] = device_id
    return response
```
