{"title":"SSRF via requests.get() on User-Controlled URL","language":"Python","severity":"High","cwe":"CWE-918","source_lines":[14],"flow_lines":[14,15,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=10)\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json(), 'device': device_endpoint}\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)}\n\ndevice_url = request.form.get('device_url')\ntoken = session.get('iot_token')\ntelemetry_data = fetch_iot_device_telemetry(device_url, token)","explanation":"The application accepts a user-controlled device URL without validation and directly uses it in requests.get(), enabling Server-Side Request Forgery (SSRF). An attacker can manipulate the device_url parameter to target internal services, cloud metadata endpoints, or perform port scanning on internal infrastructure.","remediation":"The fix adds comprehensive URL validation before making the HTTP request, including scheme enforcement (HTTPS only), DNS resolution checks against blocked internal/private IP ranges, an allowlist for registered device hosts, credential stripping prevention, and disabling of redirects to prevent redirect-based SSRF bypasses. Error messages are also sanitized to avoid leaking internal infrastructure details.","secure_code":"import requests\nimport json\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\n# Allowlist of permitted device hostname patterns or registered devices\nALLOWED_DEVICE_HOSTS = set()  # Populate from database of registered devices\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 (internal/private networks).\"\"\"\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 and points to an allowed external host.\"\"\"\n    if not device_endpoint:\n        return False, \"Device URL is required\"\n\n    try:\n        parsed = urlparse(device_endpoint)\n    except Exception:\n        return False, \"Invalid URL format\"\n\n    # Enforce HTTPS only\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False, f\"Only {ALLOWED_SCHEMES} schemes are allowed\"\n\n    hostname = parsed.hostname\n    if not hostname:\n        return False, \"No hostname found in URL\"\n\n    # Block URLs with credentials\n    if parsed.username or parsed.password:\n        return False, \"URLs with credentials are not allowed\"\n\n    # Check against allowlist of registered device hosts\n    if ALLOWED_DEVICE_HOSTS and hostname not in ALLOWED_DEVICE_HOSTS:\n        return False, \"Device host is not registered in the platform\"\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, \"Device URL resolves to a blocked internal address\"\n    except socket.gaierror:\n        return False, \"Unable to resolve device hostname\"\n\n    return True, None\n\ndef fetch_iot_device_telemetry(device_endpoint, auth_token):\n    \"\"\"Fetch telemetry from a validated IoT device endpoint.\"\"\"\n    # Validate the URL before making the request\n    is_valid, error_message = validate_device_url(device_endpoint)\n    if not is_valid:\n        return {'status': 'error', 'message': f'Invalid device URL: {error_message}'}\n\n    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}\n    try:\n        telemetry_url = f\"{device_endpoint}/api/v2/telemetry\"\n\n        # Re-validate the constructed URL to prevent path-based bypasses\n        is_valid, error_message = validate_device_url(telemetry_url.rsplit('/api/v2/telemetry', 1)[0])\n        if not is_valid:\n            return {'status': 'error', 'message': f'Invalid constructed URL: {error_message}'}\n\n        response = requests.get(telemetry_url, headers=headers, timeout=10, allow_redirects=False)\n        if response.status_code == 200:\n            return {'status': 'success', 'data': response.json(), 'device': device_endpoint}\n        return {'status': 'error', 'message': 'Device unreachable', 'code': response.status_code}\n    except requests.exceptions.RequestException:\n        return {'status': 'error', 'message': 'Failed to connect to device'}\n\ndevice_url = request.form.get('device_url')\ntoken = session.get('iot_token')\ntelemetry_data = fetch_iot_device_telemetry(device_url, token)"}