{"title":"HTTP Response Splitting (CRLF Injection)","language":"Python","severity":"High","cwe":"CWE-113","source_lines":[5,6],"flow_lines":[5,6,8,9,10],"sink_lines":[8,10],"vulnerable_code":"from flask import Flask, request, make_response\napp = Flask(__name__)\n@app.route('/iot/device/session')\ndef bind_device_session():\n    device_token = request.args.get('dtk', '')\n    region_code = request.args.get('rgn', 'us-east')\n    resp = make_response('Device session initialized')\n    resp.headers['X-Region'] = region_code\n    cookie_val = f'dtk={device_token}; Path=/; HttpOnly'\n    resp.headers['Set-Cookie'] = cookie_val\n    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":"from flask import Flask, request, make_response, abort\nimport re\n\napp = Flask(__name__)\n\ndef sanitize_header_value(value):\n    \"\"\"Remove CRLF characters and other control characters to prevent HTTP response splitting.\"\"\"\n    if re.search(r'[\\r\\n]', value):\n        abort(400, description='Invalid characters in input')\n    # Also strip any remaining control characters (except space/tab)\n    return re.sub(r'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]', '', value)\n\nALLOWED_REGIONS = {'us-east', 'us-west', 'eu-west', 'eu-central', 'ap-southeast', 'ap-northeast'}\n\n@app.route('/iot/device/session')\ndef bind_device_session():\n    device_token = request.args.get('dtk', '')\n    region_code = request.args.get('rgn', 'us-east')\n\n    # Sanitize inputs to prevent CRLF injection\n    device_token = sanitize_header_value(device_token)\n    region_code = sanitize_header_value(region_code)\n\n    # Validate region against allowlist\n    if region_code not in ALLOWED_REGIONS:\n        region_code = 'us-east'\n\n    resp = make_response('Device session initialized')\n    resp.headers['X-Region'] = region_code\n\n    # Use Flask's set_cookie() instead of raw Set-Cookie header assignment\n    resp.set_cookie('dtk', device_token, path='/', httponly=True, samesite='Lax')\n\n    return resp"}