{"title":"SSRF via Unvalidated requests.get() URL Parameter","language":"Python","severity":"Critical","cwe":"CWE-918","source_lines":[8],"flow_lines":[8,12],"sink_lines":[12],"vulnerable_code":"import requests\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/device-status', methods=['POST'])\ndef fetch_device_telemetry():\n    device_endpoint = request.json.get('telemetry_url')\n    auth_token = request.json.get('device_token', '')\n    headers = {'Authorization': f'Bearer {auth_token}', 'User-Agent': 'IoT-Gateway/2.1'}\n    try:\n        telemetry_response = requests.get(device_endpoint, headers=headers, timeout=10)\n        device_data = telemetry_response.json()\n        return jsonify({\n            'status': 'success',\n            'temperature': device_data.get('temp'),\n            'humidity': device_data.get('humidity'),\n            'uptime': device_data.get('uptime')\n        }), 200\n    except Exception as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 500","explanation":"The application accepts a user-controlled URL via 'telemetry_url' parameter without validation and directly passes it to requests.get(). This allows attackers to perform Server-Side Request Forgery (SSRF) by specifying internal network endpoints, cloud metadata services (e.g., http://169.254.169.254/latest/meta-data/), or other sensitive internal resources.","remediation":"The fix validates the user-supplied URL by checking the scheme (HTTPS only), port (restricted allowlist), and resolving the hostname to verify the IP address does not point to internal/metadata services or blocked network ranges. It also disables HTTP redirects to prevent redirect-based SSRF bypasses and uses specific exception handling to avoid leaking internal error details to the client.","secure_code":"import requests\nfrom flask import Flask, request, jsonify\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\napp = Flask(__name__)\n\n# Allowlist of permitted device network ranges (configure as needed)\nALLOWED_SCHEMES = {'https'}\nALLOWED_PORTS = {443, 8443}\n# Define allowed external subnets for IoT devices (example)\nALLOWED_DEVICE_SUBNETS = [\n    ipaddress.ip_network('192.168.100.0/24'),  # Example IoT device subnet\n    ipaddress.ip_network('10.0.50.0/24'),       # Example edge device subnet\n]\n\n# Blocked internal/metadata ranges\nBLOCKED_RANGES = [\n    ipaddress.ip_network('127.0.0.0/8'),\n    ipaddress.ip_network('169.254.0.0/16'),\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('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\n\ndef is_ip_in_allowed_subnets(ip_str):\n    \"\"\"Check if the resolved IP is within explicitly allowed device subnets.\"\"\"\n    try:\n        ip = ipaddress.ip_address(ip_str)\n        for subnet in ALLOWED_DEVICE_SUBNETS:\n            if ip in subnet:\n                return True\n        return False\n    except ValueError:\n        return False\n\n\ndef is_ip_blocked(ip_str):\n    \"\"\"Check if the resolved IP falls within blocked internal ranges.\"\"\"\n    try:\n        ip = ipaddress.ip_address(ip_str)\n        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:\n            if not is_ip_in_allowed_subnets(ip_str):\n                return True\n        for blocked in BLOCKED_RANGES:\n            if ip in blocked and not is_ip_in_allowed_subnets(ip_str):\n                return True\n        return False\n    except ValueError:\n        return True\n\n\ndef validate_device_url(url):\n    \"\"\"Validate the telemetry URL against SSRF protections.\"\"\"\n    if not url or not isinstance(url, str):\n        return False, \"Invalid URL provided\"\n\n    try:\n        parsed = urlparse(url)\n    except Exception:\n        return False, \"Malformed URL\"\n\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False, f\"Scheme '{parsed.scheme}' not allowed. Only {ALLOWED_SCHEMES} 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} not allowed. Only {ALLOWED_PORTS} permitted.\"\n\n    try:\n        resolved_ips = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)\n        if not resolved_ips:\n            return False, \"Could not resolve hostname\"\n\n        for result in resolved_ips:\n            ip_str = result[4][0]\n            if is_ip_blocked(ip_str):\n                return False, f\"Access to resolved IP address is forbidden\"\n    except socket.gaierror:\n        return False, \"DNS resolution failed for hostname\"\n\n    return True, \"Valid\"\n\n\n@app.route('/api/iot/device-status', methods=['POST'])\ndef fetch_device_telemetry():\n    if not request.json:\n        return jsonify({'status': 'error', 'message': 'Request body must be JSON'}), 400\n\n    device_endpoint = request.json.get('telemetry_url')\n    auth_token = request.json.get('device_token', '')\n\n    is_valid, validation_message = validate_device_url(device_endpoint)\n    if not is_valid:\n        return jsonify({'status': 'error', 'message': f'Invalid device endpoint: {validation_message}'}), 400\n\n    headers = {'Authorization': f'Bearer {auth_token}', 'User-Agent': 'IoT-Gateway/2.1'}\n    try:\n        telemetry_response = requests.get(\n            device_endpoint,\n            headers=headers,\n            timeout=10,\n            allow_redirects=False\n        )\n        device_data = telemetry_response.json()\n        return jsonify({\n            'status': 'success',\n            'temperature': device_data.get('temp'),\n            'humidity': device_data.get('humidity'),\n            'uptime': device_data.get('uptime')\n        }), 200\n    except requests.exceptions.RequestException as e:\n        return jsonify({'status': 'error', 'message': 'Failed to fetch device telemetry'}), 502\n    except (ValueError, KeyError):\n        return jsonify({'status': 'error', 'message': 'Invalid response from device'}), 502"}