{"title":"SSRF via requests.get on User-Controlled URL","language":"Python","severity":"High","cwe":"CWE-918","source_lines":[8],"flow_lines":[8,11],"sink_lines":[11],"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}'}\n    response = requests.get(device_endpoint, headers=headers, timeout=5)\n    telemetry_data = response.json()\n    return jsonify({'status': 'success', 'device_data': telemetry_data, 'response_code': response.status_code})","explanation":"The application accepts a user-controlled 'telemetry_url' parameter and directly passes it to requests.get() without validation. This allows attackers to make the server send HTTP requests to arbitrary URLs, including internal network resources, cloud metadata endpoints, or other services not directly accessible to the attacker.","remediation":"The fix implements multiple layers of SSRF protection: URL validation enforces an allowlist of permitted device domains and HTTPS-only scheme, DNS resolution checking blocks requests to private/internal IP ranges (including cloud metadata endpoints like 169.254.x.x), and redirects are disabled to prevent redirect-based bypasses. Additionally, proper error handling prevents information leakage.","secure_code":"import requests\nimport ipaddress\nfrom urllib.parse import urlparse\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n# Allowlist of permitted device hostname patterns or domains\nALLOWED_DEVICE_DOMAINS = [\n    'devices.example.com',\n    'iot.example.com',\n    'sensors.example.com',\n]\n\nALLOWED_SCHEMES = ['https']\n\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\n\ndef is_ip_blocked(hostname):\n    \"\"\"Check if a hostname resolves to a blocked IP range.\"\"\"\n    import socket\n    try:\n        addr_info = socket.getaddrinfo(hostname, None)\n        for entry in addr_info:\n            ip = ipaddress.ip_address(entry[4][0])\n            for blocked_range in BLOCKED_IP_RANGES:\n                if ip in blocked_range:\n                    return True\n    except (socket.gaierror, ValueError):\n        return True\n    return False\n\n\ndef validate_url(url):\n    \"\"\"Validate that the URL is safe to request.\"\"\"\n    if not url or not isinstance(url, str):\n        return False, \"Invalid or missing URL\"\n\n    try:\n        parsed = urlparse(url)\n    except Exception:\n        return False, \"Malformed URL\"\n\n    # Check scheme\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False, f\"URL scheme '{parsed.scheme}' is not allowed. Only {ALLOWED_SCHEMES} permitted.\"\n\n    # Extract hostname\n    hostname = parsed.hostname\n    if not hostname:\n        return False, \"No hostname found in URL\"\n\n    # Check against allowed domains\n    domain_allowed = False\n    for allowed_domain in ALLOWED_DEVICE_DOMAINS:\n        if hostname == allowed_domain or hostname.endswith('.' + allowed_domain):\n            domain_allowed = True\n            break\n\n    if not domain_allowed:\n        return False, f\"Hostname '{hostname}' is not in the allowed device domains\"\n\n    # Check that hostname doesn't resolve to a private/internal IP\n    if is_ip_blocked(hostname):\n        return False, \"URL resolves to a blocked internal IP address\"\n\n    # Disallow non-standard ports\n    if parsed.port and parsed.port not in (443,):\n        return False, f\"Port {parsed.port} is not allowed\"\n\n    return True, None\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    # Validate the URL before making any request\n    is_valid, error_message = validate_url(device_endpoint)\n    if not is_valid:\n        return jsonify({'status': 'error', 'message': error_message}), 400\n\n    headers = {'Authorization': f'Bearer {auth_token}'}\n\n    try:\n        response = requests.get(device_endpoint, headers=headers, timeout=5, allow_redirects=False)\n        telemetry_data = response.json()\n        return jsonify({'status': 'success', 'device_data': telemetry_data, 'response_code': response.status_code})\n    except requests.exceptions.RequestException as e:\n        return jsonify({'status': 'error', 'message': 'Failed to reach device endpoint'}), 502\n    except ValueError:\n        return jsonify({'status': 'error', 'message': 'Device returned invalid JSON'}), 502"}