# HTTP Header Injection via CRLF Sequence

Language: Python
Severity: High
CWE: CWE-113

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
from flask import Flask, request, Response
app = Flask(__name__)
@app.route('/iot/device/redirect')
def device_portal_redirect():
    dev_callback = request.args.get('callback_url', '/')
    fw_version = request.args.get('fw_ver', 'unknown')
    resp_hdrs = {
        'X-Firmware-Version': fw_version,
        'Cache-Control': 'no-store'
    }
    raw_resp = Response(status=302)
    for hdr_key, hdr_val in resp_hdrs.items():
        raw_resp.headers[hdr_key] = hdr_val
    raw_resp.headers['Location'] = dev_callback
    return raw_resp
```

## Explanation

Both `callback_url` and `fw_ver` query parameters are read directly from user-supplied HTTP request arguments without any sanitization of CRLF characters (\r\n). These values are then assigned directly into HTTP response headers (X-Firmware-Version and Location) via raw_resp.headers[hdr_key] = hdr_val and raw_resp.headers['Location'] = dev_callback, allowing an attacker to inject arbitrary headers or split the HTTP response by embedding CRLF sequences in either parameter.

## Remediation

The fix introduces a `sanitize_header_value` function that strips all CR, LF, and other control characters from any user-supplied value before it is placed into an HTTP response header, preventing CRLF injection. Additionally, the callback URL is validated against an allowlist of permitted hostnames and restricted to safe schemes (http/https) to prevent open redirect attacks. If the callback URL fails validation, the server returns a 400 Bad Request instead of performing the redirect.

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

app = Flask(__name__)

ALLOWED_CALLBACK_HOSTS = {'portal.example.com', 'devices.example.com'}

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

def validate_callback_url(url):
    """Validate that the callback URL is safe and belongs to an allowed host."""
    try:
        parsed = urlparse(url)
        if parsed.scheme and parsed.scheme not in ('http', 'https'):
            return None
        if parsed.hostname and parsed.hostname not in ALLOWED_CALLBACK_HOSTS:
            return None
        # Sanitize any residual CRLF characters
        return sanitize_header_value(url)
    except Exception:
        return None

@app.route('/iot/device/redirect')
def device_portal_redirect():
    dev_callback = request.args.get('callback_url', '/')
    fw_version = request.args.get('fw_ver', 'unknown')

    # Sanitize firmware version header value against CRLF injection
    fw_version = sanitize_header_value(fw_version)

    # Validate and sanitize callback URL
    safe_callback = validate_callback_url(dev_callback)
    if safe_callback is None:
        abort(400, description='Invalid callback URL')

    resp_hdrs = {
        'X-Firmware-Version': fw_version,
        'Cache-Control': 'no-store'
    }
    raw_resp = Response(status=302)
    for hdr_key, hdr_val in resp_hdrs.items():
        raw_resp.headers[hdr_key] = hdr_val
    raw_resp.headers['Location'] = safe_callback
    return raw_resp
```
