{"title":"CRLF Injection via Untrusted HTTP Response Headers","language":"Python","severity":"High","cwe":"CWE-93","source_lines":[11,14],"flow_lines":[11,22,14,23],"sink_lines":[22,23],"vulnerable_code":"from flask import Flask, request, Response\nimport boto3\n\napp = Flask(__name__)\ns3_client = boto3.client('s3')\n\n@app.route('/cloud/presigned-download')\ndef generate_presigned_url():\n    bucket_name = request.args.get('bucket', 'default-storage')\n    object_key = request.args.get('key')\n    user_agent = request.headers.get('User-Agent', 'unknown')\n    \n    presigned_url = s3_client.generate_presigned_url(\n        'get_object',\n        Params={'Bucket': bucket_name, 'Key': object_key},\n        ExpiresIn=3600\n    )\n    \n    download_filename = request.args.get('filename', 'download.bin')\n    \n    resp = Response(presigned_url)\n    resp.headers['X-Download-Filename'] = download_filename\n    resp.headers['X-Client-Agent'] = user_agent\n    resp.headers['Content-Type'] = 'text/plain'\n    \n    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":"from flask import Flask, request, Response\nimport boto3\nimport re\n\napp = Flask(__name__)\ns3_client = boto3.client('s3')\n\ndef sanitize_header_value(value):\n    \"\"\"Remove CRLF characters and other control characters from header values.\"\"\"\n    if value is None:\n        return ''\n    # Remove carriage return, line feed, and other control characters\n    sanitized = re.sub(r'[\\r\\n\\x00-\\x1f\\x7f]', '', value)\n    return sanitized\n\ndef sanitize_filename(filename):\n    \"\"\"Sanitize filename for use in headers, allowing only safe characters.\"\"\"\n    if filename is None:\n        return 'download.bin'\n    # Remove any characters that are not alphanumeric, dots, hyphens, or underscores\n    sanitized = re.sub(r'[^a-zA-Z0-9._\\-]', '_', filename)\n    # Prevent empty filename\n    if not sanitized:\n        return 'download.bin'\n    return sanitized\n\n@app.route('/cloud/presigned-download')\ndef generate_presigned_url():\n    bucket_name = request.args.get('bucket', 'default-storage')\n    object_key = request.args.get('key')\n    user_agent = request.headers.get('User-Agent', 'unknown')\n    \n    if not object_key:\n        return Response('Missing required parameter: key', status=400)\n    \n    presigned_url = s3_client.generate_presigned_url(\n        'get_object',\n        Params={'Bucket': bucket_name, 'Key': object_key},\n        ExpiresIn=3600\n    )\n    \n    download_filename = request.args.get('filename', 'download.bin')\n    \n    # Sanitize user-controlled values before setting them as headers\n    safe_filename = sanitize_filename(download_filename)\n    safe_user_agent = sanitize_header_value(user_agent)\n    \n    resp = Response(presigned_url)\n    resp.headers['X-Download-Filename'] = safe_filename\n    resp.headers['X-Client-Agent'] = safe_user_agent\n    resp.headers['Content-Type'] = 'text/plain'\n    \n    return resp"}