{"title":"HTTP Header Injection via CRLF Sequence","language":"Python","severity":"High","cwe":"CWE-113","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from flask import Flask, request, Response\napp = Flask(__name__)\n@app.route('/iot/device/redirect')\ndef device_portal_redirect():\n    dev_callback = request.args.get('callback_url', '/')\n    fw_version = request.args.get('fw_ver', 'unknown')\n    resp_hdrs = {\n        'X-Firmware-Version': fw_version,\n        'Cache-Control': 'no-store'\n    }\n    raw_resp = Response(status=302)\n    for hdr_key, hdr_val in resp_hdrs.items():\n        raw_resp.headers[hdr_key] = hdr_val\n    raw_resp.headers['Location'] = dev_callback\n    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":"from flask import Flask, request, Response, abort\nimport re\nfrom urllib.parse import urlparse\n\napp = Flask(__name__)\n\nALLOWED_CALLBACK_HOSTS = {'portal.example.com', 'devices.example.com'}\n\ndef sanitize_header_value(value):\n    \"\"\"Remove any CRLF characters and other control characters from header values.\"\"\"\n    return re.sub(r'[\\r\\n\\x00-\\x1f\\x7f]', '', value)\n\ndef validate_callback_url(url):\n    \"\"\"Validate that the callback URL is safe and belongs to an allowed host.\"\"\"\n    try:\n        parsed = urlparse(url)\n        if parsed.scheme and parsed.scheme not in ('http', 'https'):\n            return None\n        if parsed.hostname and parsed.hostname not in ALLOWED_CALLBACK_HOSTS:\n            return None\n        # Sanitize any residual CRLF characters\n        return sanitize_header_value(url)\n    except Exception:\n        return None\n\n@app.route('/iot/device/redirect')\ndef device_portal_redirect():\n    dev_callback = request.args.get('callback_url', '/')\n    fw_version = request.args.get('fw_ver', 'unknown')\n\n    # Sanitize firmware version header value against CRLF injection\n    fw_version = sanitize_header_value(fw_version)\n\n    # Validate and sanitize callback URL\n    safe_callback = validate_callback_url(dev_callback)\n    if safe_callback is None:\n        abort(400, description='Invalid callback URL')\n\n    resp_hdrs = {\n        'X-Firmware-Version': fw_version,\n        'Cache-Control': 'no-store'\n    }\n    raw_resp = Response(status=302)\n    for hdr_key, hdr_val in resp_hdrs.items():\n        raw_resp.headers[hdr_key] = hdr_val\n    raw_resp.headers['Location'] = safe_callback\n    return raw_resp"}