# HTTP Response Splitting (CRLF Injection)

Language: Python
Severity: High
CWE: CWE-113

## Source
5-6

## Flow
5-6-8-9-10

## Sink
8, 10

## Vulnerable Code
```python
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/iot/device/session')
def bind_device_session():
    device_token = request.args.get('dtk', '')
    region_code = request.args.get('rgn', 'us-east')
    resp = make_response('Device session initialized')
    resp.headers['X-Region'] = region_code
    cookie_val = f'dtk={device_token}; Path=/; HttpOnly'
    resp.headers['Set-Cookie'] = cookie_val
    return resp
```

## Explanation

Both `device_token` (line 5) and `region_code` (line 6) are taken directly from user-controlled query parameters without any sanitization or stripping of CRLF characters (\r\n / %0d%0a). The `region_code` is directly assigned to `resp.headers['X-Region']` (line 8), and `device_token` is embedded into a raw `Set-Cookie` header string (line 9) which is then assigned to `resp.headers['Set-Cookie']` (line 10). Both header assignments serve as CRLF injection sinks, allowing an attacker to inject arbitrary HTTP headers or split the HTTP response.

## Remediation

The fix sanitizes both user-controlled inputs (`device_token` and `region_code`) by detecting and rejecting any CRLF characters (`\r`, `\n`) before they reach HTTP header assignments, preventing response splitting. Additionally, `region_code` is validated against an allowlist of permitted regions, and the raw `Set-Cookie` header assignment is replaced with Flask's built-in `response.set_cookie()` method which properly handles encoding and escaping of cookie values.

## Secure Code
```python
from flask import Flask, request, make_response, abort
import re

app = Flask(__name__)

def sanitize_header_value(value):
    """Remove CRLF characters and other control characters to prevent HTTP response splitting."""
    if re.search(r'[\r\n]', value):
        abort(400, description='Invalid characters in input')
    # Also strip any remaining control characters (except space/tab)
    return re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', value)

ALLOWED_REGIONS = {'us-east', 'us-west', 'eu-west', 'eu-central', 'ap-southeast', 'ap-northeast'}

@app.route('/iot/device/session')
def bind_device_session():
    device_token = request.args.get('dtk', '')
    region_code = request.args.get('rgn', 'us-east')

    # Sanitize inputs to prevent CRLF injection
    device_token = sanitize_header_value(device_token)
    region_code = sanitize_header_value(region_code)

    # Validate region against allowlist
    if region_code not in ALLOWED_REGIONS:
        region_code = 'us-east'

    resp = make_response('Device session initialized')
    resp.headers['X-Region'] = region_code

    # Use Flask's set_cookie() instead of raw Set-Cookie header assignment
    resp.set_cookie('dtk', device_token, path='/', httponly=True, samesite='Lax')

    return resp
```
