{"title":"Server-Side Request Forgery via requests.get on User-Controlled URL","language":"Python","severity":"High","cwe":"CWE-918","source_lines":[4],"flow_lines":[4,6,8],"sink_lines":[8],"vulnerable_code":"import requests\nimport json\n\ndef fetch_iot_device_telemetry(device_endpoint, auth_token):\n    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}\n    telemetry_url = f\"{device_endpoint}/api/v2/telemetry/current\"\n    try:\n        response = requests.get(telemetry_url, headers=headers, timeout=5)\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json(), 'source': telemetry_url}\n        return {'status': 'error', 'message': 'Device unreachable', 'code': response.status_code}\n    except requests.exceptions.RequestException as e:\n        return {'status': 'error', 'message': str(e)}","explanation":"The function accepts a user-controlled 'device_endpoint' parameter without validation and directly uses it to construct a URL that is passed to requests.get(). This allows an attacker to make the server send HTTP requests to arbitrary internal or external systems, enabling Server-Side Request Forgery (SSRF) attacks to scan internal networks, access cloud metadata services, or exfiltrate data.","remediation":"The fix adds comprehensive URL validation before making the HTTP request. It restricts the scheme to HTTPS only, resolves the hostname to check that all resolved IP addresses are not in private/internal network ranges (blocking access to cloud metadata services, localhost, and internal networks), restricts allowed ports, disables redirects to prevent redirect-based SSRF bypasses, and removes the source URL and raw exception details from responses to avoid information leakage.","secure_code":"import requests\nimport json\nimport ipaddress\nfrom urllib.parse import urlparse\nimport socket\n\nALLOWED_SCHEMES = {'https'}\nBLOCKED_NETWORKS = [\n    ipaddress.ip_network('127.0.0.0/8'),\n    ipaddress.ip_network('10.0.0.0/8'),\n    ipaddress.ip_network('172.16.0.0/12'),\n    ipaddress.ip_network('192.168.0.0/16'),\n    ipaddress.ip_network('169.254.0.0/16'),\n    ipaddress.ip_network('0.0.0.0/8'),\n    ipaddress.ip_network('::1/128'),\n    ipaddress.ip_network('fc00::/7'),\n    ipaddress.ip_network('fe80::/10'),\n]\n\nALLOWED_PORTS = {443, 8443}\n\n\ndef is_safe_url(url):\n    \"\"\"Validate that the URL does not point to internal/private resources.\"\"\"\n    try:\n        parsed = urlparse(url)\n    except ValueError:\n        return False, \"Invalid URL format\"\n\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False, f\"Scheme '{parsed.scheme}' is not allowed. Only HTTPS is permitted.\"\n\n    hostname = parsed.hostname\n    if not hostname:\n        return False, \"No hostname found in URL\"\n\n    port = parsed.port or (443 if parsed.scheme == 'https' else 80)\n    if port not in ALLOWED_PORTS:\n        return False, f\"Port {port} is not allowed\"\n\n    try:\n        addrinfos = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)\n    except socket.gaierror:\n        return False, \"Unable to resolve hostname\"\n\n    for addrinfo in addrinfos:\n        ip = ipaddress.ip_address(addrinfo[4][0])\n        for blocked_network in BLOCKED_NETWORKS:\n            if ip in blocked_network:\n                return False, f\"Access to private/internal network addresses is not allowed\"\n\n    return True, None\n\n\ndef fetch_iot_device_telemetry(device_endpoint, auth_token):\n    \"\"\"Retrieve real-time telemetry data from a validated IoT device endpoint.\"\"\"\n    device_endpoint = device_endpoint.rstrip('/')\n    telemetry_url = f\"{device_endpoint}/api/v2/telemetry/current\"\n\n    is_safe, error_message = is_safe_url(telemetry_url)\n    if not is_safe:\n        return {'status': 'error', 'message': f'Invalid device endpoint: {error_message}'}\n\n    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}\n    try:\n        response = requests.get(\n            telemetry_url,\n            headers=headers,\n            timeout=5,\n            allow_redirects=False\n        )\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json()}\n        return {'status': 'error', 'message': 'Device unreachable', 'code': response.status_code}\n    except requests.exceptions.RequestException as e:\n        return {'status': 'error', 'message': 'Request failed'}"}