{"title":"SSRF via Unvalidated requests.get() URL Fetching","language":"Python","severity":"High","cwe":"CWE-918","source_lines":[8],"flow_lines":[8,10],"sink_lines":[10],"vulnerable_code":"import requests\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/api/iot/device/health', methods=['POST'])\ndef check_device_health():\n    device_endpoint = request.json.get('callback_url')\n    device_id = request.json.get('device_id', 'unknown')\n    try:\n        health_response = requests.get(device_endpoint, timeout=5)\n        return jsonify({'device_id': device_id, 'status': health_response.status_code, 'data': health_response.text[:200]})\n    except requests.RequestException as e:\n        return jsonify({'device_id': device_id, 'error': str(e)}), 500","explanation":"The application accepts a user-controlled 'callback_url' from the JSON request body without any validation and directly passes it to requests.get(). This allows an attacker to force the server to make HTTP requests to arbitrary internal or external URLs, enabling Server-Side Request Forgery (SSRF) attacks against internal infrastructure or metadata endpoints.","remediation":"The fix introduces a URL validation function that parses the callback URL, ensures only http/https schemes are allowed, resolves the hostname to its IP addresses, and checks each resolved IP against a blocklist of private/internal networks (including the cloud metadata endpoint 169.254.169.254). Additionally, redirects are disabled to prevent bypass via open redirects, and URLs with embedded credentials are rejected.","secure_code":"import requests\nfrom flask import Flask, request, jsonify\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\napp = Flask(__name__)\n\nALLOWED_SCHEMES = {'http', '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\n\ndef is_safe_url(url):\n    \"\"\"Validate that the URL does not point to internal/private network resources.\"\"\"\n    try:\n        parsed = urlparse(url)\n    except Exception:\n        return False\n\n    if not parsed.hostname:\n        return False\n\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False\n\n    if parsed.username or parsed.password:\n        return False\n\n    try:\n        addrinfos = socket.getaddrinfo(parsed.hostname, parsed.port or 80)\n    except socket.gaierror:\n        return False\n\n    for addrinfo in addrinfos:\n        ip = ipaddress.ip_address(addrinfo[4][0])\n        for network in BLOCKED_NETWORKS:\n            if ip in network:\n                return False\n\n    return True\n\n\n@app.route('/api/iot/device/health', methods=['POST'])\ndef check_device_health():\n    device_endpoint = request.json.get('callback_url')\n    device_id = request.json.get('device_id', 'unknown')\n\n    if not device_endpoint or not isinstance(device_endpoint, str):\n        return jsonify({'device_id': device_id, 'error': 'Missing or invalid callback_url'}), 400\n\n    if not is_safe_url(device_endpoint):\n        return jsonify({'device_id': device_id, 'error': 'Invalid or disallowed callback URL'}), 403\n\n    try:\n        health_response = requests.get(device_endpoint, timeout=5, allow_redirects=False)\n        return jsonify({'device_id': device_id, 'status': health_response.status_code, 'data': health_response.text[:200]})\n    except requests.RequestException as e:\n        return jsonify({'device_id': device_id, 'error': str(e)}), 500"}