{"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,7,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    try:\n        telemetry_url = f\"{device_endpoint}/api/v2/telemetry\"\n        response = requests.get(telemetry_url, headers=headers, timeout=5)\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json()}\n        return {'status': 'error', 'message': 'Device unreachable'}\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 for requests.get(). This allows attackers to make the server send HTTP requests to arbitrary internal or external destinations, potentially accessing internal services, cloud metadata endpoints, or exfiltrating authentication tokens.","remediation":"The fix adds a URL validation function that checks the scheme (HTTPS only), resolves the hostname to IP addresses, and verifies none of the resolved IPs belong to private, loopback, or link-local networks. It also restricts allowed ports, disables automatic redirects to prevent redirect-based SSRF bypasses, and rejects any endpoint that fails validation before making the HTTP request.","secure_code":"import requests\nimport json\nfrom urllib.parse import urlparse\nimport ipaddress\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 Exception:\n        return False\n\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False\n\n    hostname = parsed.hostname\n    if not hostname:\n        return False\n\n    port = parsed.port or (443 if parsed.scheme == 'https' else 80)\n    if port not in ALLOWED_PORTS:\n        return False\n\n    try:\n        resolved_ips = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)\n    except socket.gaierror:\n        return False\n\n    for entry in resolved_ips:\n        ip_str = entry[4][0]\n        try:\n            ip = ipaddress.ip_address(ip_str)\n        except ValueError:\n            return False\n        for blocked_net in BLOCKED_NETWORKS:\n            if ip in blocked_net:\n                return False\n\n    return True\n\n\ndef fetch_iot_device_telemetry(device_endpoint, auth_token):\n    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}\n    try:\n        telemetry_url = f\"{device_endpoint}/api/v2/telemetry\"\n\n        if not is_safe_url(telemetry_url):\n            return {'status': 'error', 'message': 'Invalid or blocked device endpoint'}\n\n        response = requests.get(telemetry_url, headers=headers, timeout=5, allow_redirects=False)\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json()}\n        return {'status': 'error', 'message': 'Device unreachable'}\n    except requests.exceptions.RequestException as e:\n        return {'status': 'error', 'message': str(e)}"}