{"title":"Server-Side Request Forgery (SSRF)","language":"Python","severity":"Critical","cwe":"CWE-918","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from flask import Flask, request, jsonify\nimport requests\napp = Flask(__name__)\n@app.route('/iot/device/telemetry', methods=['POST'])\ndef fetch_device_telemetry():\n    dev_cfg = request.get_json()\n    endpoint_url = dev_cfg.get('metrics_endpoint')\n    auth_hdr = dev_cfg.get('auth_token', '')\n    hdrs = {'Authorization': f'Bearer {auth_hdr}', 'Accept': 'application/json'}\n    resp = requests.get(endpoint_url, headers=hdrs, timeout=5)\n    telemetry_data = resp.json()\n    return jsonify({'status': 'ok', 'payload': telemetry_data})","explanation":"The 'metrics_endpoint' value is taken directly from user-supplied JSON (dev_cfg) on line 7 without any validation or allowlist check, and is passed as-is into requests.get() on line 10. This allows an attacker to supply any arbitrary URL, including internal metadata services or private network endpoints, causing the server to make requests on the attacker's behalf. The forwarding of the auth token in the headers further amplifies the impact.","remediation":"The fix introduces a comprehensive URL validation function (is_safe_url) that enforces HTTPS-only schemes, checks the hostname against an explicit allowlist of trusted IoT device hosts, resolves the hostname to its IP addresses and rejects any that fall within private, loopback, link-local (including the 169.254.0.0/16 cloud metadata range), or other internal IP ranges. Additionally, allow_redirects=False is set on the request to prevent open-redirect-based SSRF bypasses, and proper input validation and error handling are added.","secure_code":"from flask import Flask, request, jsonify\nimport requests\nfrom urllib.parse import urlparse\nimport ipaddress\nimport socket\n\napp = Flask(__name__)\n\n# Allowlist of permitted domains/hosts for device telemetry endpoints\nALLOWED_HOSTS = {\n    # Add your trusted IoT device hosts/domains here\n    # e.g., 'devices.example.com', 'telemetry.iot.example.com'\n}\n\nALLOWED_SCHEMES = {'https'}\n\nBLOCKED_IP_RANGES = [\n    ipaddress.ip_network('127.0.0.0/8'),       # Loopback\n    ipaddress.ip_network('10.0.0.0/8'),        # Private\n    ipaddress.ip_network('172.16.0.0/12'),     # Private\n    ipaddress.ip_network('192.168.0.0/16'),    # Private\n    ipaddress.ip_network('169.254.0.0/16'),    # Link-local / cloud metadata\n    ipaddress.ip_network('100.64.0.0/10'),     # Shared address space\n    ipaddress.ip_network('0.0.0.0/8'),         # \"This\" network\n    ipaddress.ip_network('::1/128'),           # IPv6 loopback\n    ipaddress.ip_network('fc00::/7'),          # IPv6 private\n    ipaddress.ip_network('fe80::/10'),         # IPv6 link-local\n]\n\n\ndef is_safe_url(url):\n    \"\"\"Validate that the URL is safe to request: scheme, host allowlist, and IP block check.\"\"\"\n    try:\n        parsed = urlparse(url)\n    except Exception:\n        return False\n\n    # Enforce allowed schemes (HTTPS only)\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        return False\n\n    hostname = parsed.hostname\n    if not hostname:\n        return False\n\n    # Reject URLs with credentials embedded (e.g., http://user:pass@host)\n    if parsed.username or parsed.password:\n        return False\n\n    # Check against the host allowlist\n    if ALLOWED_HOSTS and hostname not in ALLOWED_HOSTS:\n        return False\n\n    # Resolve the hostname and check all resolved IPs against blocked ranges\n    try:\n        addr_infos = socket.getaddrinfo(hostname, parsed.port or 443)\n    except socket.gaierror:\n        return False\n\n    for family, _, _, _, sockaddr in addr_infos:\n        ip = ipaddress.ip_address(sockaddr[0])\n        for blocked_range in BLOCKED_IP_RANGES:\n            if ip in blocked_range:\n                return False\n\n    return True\n\n\n@app.route('/iot/device/telemetry', methods=['POST'])\ndef fetch_device_telemetry():\n    dev_cfg = request.get_json()\n    if not dev_cfg or not isinstance(dev_cfg, dict):\n        return jsonify({'status': 'error', 'message': 'Invalid request body'}), 400\n\n    endpoint_url = dev_cfg.get('metrics_endpoint')\n    if not endpoint_url or not isinstance(endpoint_url, str):\n        return jsonify({'status': 'error', 'message': 'Missing or invalid metrics_endpoint'}), 400\n\n    if not is_safe_url(endpoint_url):\n        return jsonify({'status': 'error', 'message': 'The provided metrics_endpoint is not allowed'}), 403\n\n    auth_hdr = dev_cfg.get('auth_token', '')\n    hdrs = {'Authorization': f'Bearer {auth_hdr}', 'Accept': 'application/json'}\n\n    try:\n        resp = requests.get(endpoint_url, headers=hdrs, timeout=5, allow_redirects=False)\n        resp.raise_for_status()\n        telemetry_data = resp.json()\n    except requests.RequestException as e:\n        return jsonify({'status': 'error', 'message': f'Failed to fetch telemetry: {str(e)}'}), 502\n\n    return jsonify({'status': 'ok', 'payload': telemetry_data})"}