{"title":"Server-Side Request Forgery via requests.get on Untrusted URL","language":"Python","severity":"High","cwe":"CWE-918","source_lines":[16],"flow_lines":[16,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/latest\"\n        response = requests.get(telemetry_url, headers=headers, timeout=5)\n        if response.status_code == 200:\n            return json.loads(response.text)\n        return {'error': 'Device unreachable', 'status': response.status_code}\n    except requests.exceptions.RequestException as e:\n        return {'error': str(e)}\n\ndevice_url = input(\"Enter IoT device URL: \")\ntoken = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"\ndata = fetch_iot_device_telemetry(device_url, token)","explanation":"The application accepts an unsanitized URL from user input (line 16) and directly uses it to make an HTTP request (line 8) without any validation or allowlisting. This enables Server-Side Request Forgery (SSRF) attacks where an attacker can force the server to make requests to internal resources, cloud metadata endpoints, or arbitrary external systems.","remediation":"The fix adds a comprehensive URL validation function that checks the scheme (HTTPS only), verifies the hostname against an allowlist of permitted IoT device domains, resolves the hostname to verify the IP address does not fall within private/internal/metadata IP ranges, and disables HTTP redirects to prevent redirect-based SSRF bypasses. This defense-in-depth approach prevents attackers from accessing internal services, cloud metadata endpoints, or arbitrary external systems.","secure_code":"import requests\nimport json\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\n# Allowlist of permitted device hostname patterns or specific hosts\nALLOWED_HOSTS_SUFFIX = ['.iot.company.com', '.devices.internal.company.com']\nALLOWED_SCHEMES = ['https']\nBLOCKED_IP_RANGES = [\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\ndef is_ip_blocked(ip_str):\n    \"\"\"Check if an IP address falls within blocked ranges.\"\"\"\n    try:\n        ip = ipaddress.ip_address(ip_str)\n        for network in BLOCKED_IP_RANGES:\n            if ip in network:\n                return True\n        return False\n    except ValueError:\n        return True\n\ndef validate_device_url(device_endpoint):\n    \"\"\"Validate that the device URL is safe to connect to.\"\"\"\n    try:\n        parsed = urlparse(device_endpoint)\n    except Exception:\n        return False, \"Invalid URL format\"\n\n    # Check scheme\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False, f\"Scheme '{parsed.scheme}' not allowed. Use HTTPS.\"\n\n    # Check that hostname exists\n    hostname = parsed.hostname\n    if not hostname:\n        return False, \"No hostname provided\"\n\n    # Check against allowlisted host suffixes\n    host_allowed = any(hostname.endswith(suffix) for suffix in ALLOWED_HOSTS_SUFFIX)\n    if not host_allowed:\n        return False, f\"Host '{hostname}' is not in the allowed device hosts list\"\n\n    # Resolve hostname and check against blocked IP ranges\n    try:\n        resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443)\n        for entry in resolved_ips:\n            ip_str = entry[4][0]\n            if is_ip_blocked(ip_str):\n                return False, f\"Resolved IP '{ip_str}' is in a blocked range\"\n    except socket.gaierror:\n        return False, f\"Unable to resolve hostname '{hostname}'\"\n\n    # Disallow path traversal or unexpected characters in the base URL\n    if parsed.path and parsed.path != '/' and parsed.path != '':\n        if '..' in parsed.path or '//' in parsed.path:\n            return False, \"Suspicious path detected in device URL\"\n\n    return True, \"Valid\"\n\ndef fetch_iot_device_telemetry(device_endpoint, auth_token):\n    # Validate URL before making request\n    is_valid, reason = validate_device_url(device_endpoint)\n    if not is_valid:\n        return {'error': f'Invalid device URL: {reason}'}\n\n    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}\n    try:\n        telemetry_url = f\"{device_endpoint}/api/v2/telemetry/latest\"\n        response = requests.get(telemetry_url, headers=headers, timeout=5, allow_redirects=False)\n        if response.status_code == 200:\n            return json.loads(response.text)\n        return {'error': 'Device unreachable', 'status': response.status_code}\n    except requests.exceptions.RequestException as e:\n        return {'error': str(e)}\n\ndevice_url = input(\"Enter IoT device URL: \").strip()\ntoken = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"\ndata = fetch_iot_device_telemetry(device_url, token)"}