{"title":"Server-Side Request Forgery (SSRF)","language":"Python","severity":"Critical","cwe":"CWE-918","source_lines":[7],"flow_lines":[7,8,9],"sink_lines":[9],"vulnerable_code":"from flask import Flask, request, jsonify\nimport requests\napp = Flask(__name__)\n@app.route('/iot/device/metrics', methods=['POST'])\ndef fetch_device_telemetry():\n    dev_payload = request.get_json()\n    endpoint_url = dev_payload.get('telemetry_src')\n    auth_hdr = {'X-Device-Token': dev_payload.get('tok', 'none')}\n    raw_resp = requests.get(endpoint_url, headers=auth_hdr, timeout=5)\n    sensor_data = raw_resp.json()\n    return jsonify({'status': 'ok', 'metrics': sensor_data})","explanation":"The application extracts 'telemetry_src' directly from user-controlled JSON input without any validation or sanitization, then passes it directly to requests.get(). An attacker can supply any arbitrary URL — including internal cloud metadata endpoints or internal network services — causing the server to make requests on the attacker's behalf, enabling SSRF attacks that can exfiltrate cloud credentials or probe internal infrastructure.","remediation":"The fix implements a multi-layered SSRF defense: (1) a strict allowlist of permitted telemetry host domains ensures only pre-approved sources are contacted, (2) the scheme is restricted to HTTPS only, (3) raw IP addresses in URLs are rejected outright, (4) DNS resolution results are checked against blocked IP ranges (private, loopback, link-local, metadata IPs like 169.254.169.254) to prevent DNS rebinding attacks, and (5) HTTP redirects are disabled to prevent redirect-based SSRF bypasses. Error handling is also improved to avoid leaking internal details.","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 telemetry source domains/hosts\nALLOWED_TELEMETRY_HOSTS = {\n    \"telemetry.iot-platform.example.com\",\n    \"metrics.iot-platform.example.com\",\n}\n\nALLOWED_SCHEMES = {\"https\"}\n\n# Blocked IP ranges (internal, link-local, loopback, cloud metadata, etc.)\ndef _is_blocked_ip(ip_str):\n    try:\n        addr = ipaddress.ip_address(ip_str)\n        return (\n            addr.is_private\n            or addr.is_loopback\n            or addr.is_link_local\n            or addr.is_reserved\n            or addr.is_multicast\n            or addr.is_unspecified\n        )\n    except ValueError:\n        return True  # If it can't be parsed, block it\n\n\ndef _validate_telemetry_url(url):\n    \"\"\"Validate that the telemetry URL is safe to fetch.\n    \n    Returns the URL if valid, raises ValueError otherwise.\n    \"\"\"\n    if not url or not isinstance(url, str):\n        raise ValueError(\"Missing or invalid telemetry_src URL.\")\n\n    parsed = urlparse(url)\n\n    # 1. Enforce HTTPS only\n    if parsed.scheme not in ALLOWED_SCHEMES:\n        raise ValueError(f\"Scheme '{parsed.scheme}' is not allowed. Only {ALLOWED_SCHEMES} permitted.\")\n\n    hostname = parsed.hostname\n    if not hostname:\n        raise ValueError(\"No hostname found in telemetry_src URL.\")\n\n    # 2. Block raw IP addresses in the URL to prevent direct internal IP access\n    try:\n        ipaddress.ip_address(hostname)\n        raise ValueError(\"IP addresses are not allowed as telemetry source hosts. Use a domain name.\")\n    except ValueError as e:\n        if \"not allowed\" in str(e):\n            raise\n        # hostname is not an IP literal — continue with domain validation\n\n    # 3. Check against the allowlist of permitted domains\n    if hostname not in ALLOWED_TELEMETRY_HOSTS:\n        raise ValueError(f\"Host '{hostname}' is not in the allowed telemetry sources list.\")\n\n    # 4. Resolve the hostname and verify it doesn't point to a blocked IP\n    try:\n        resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443)\n    except socket.gaierror:\n        raise ValueError(f\"Could not resolve hostname '{hostname}'.\")\n\n    for family, kind, proto, canonname, sockaddr in resolved_ips:\n        ip_str = sockaddr[0]\n        if _is_blocked_ip(ip_str):\n            raise ValueError(\n                f\"Host '{hostname}' resolves to a blocked/internal IP address ({ip_str}). Request denied.\"\n            )\n\n    return url\n\n\n@app.route('/iot/device/metrics', methods=['POST'])\ndef fetch_device_telemetry():\n    dev_payload = request.get_json()\n    if not dev_payload:\n        return jsonify({'status': 'error', 'message': 'Invalid JSON payload.'}), 400\n\n    endpoint_url = dev_payload.get('telemetry_src')\n\n    # Validate and sanitize the telemetry source URL\n    try:\n        validated_url = _validate_telemetry_url(endpoint_url)\n    except ValueError as e:\n        return jsonify({'status': 'error', 'message': str(e)}), 400\n\n    auth_hdr = {'X-Device-Token': dev_payload.get('tok', 'none')}\n\n    try:\n        raw_resp = requests.get(\n            validated_url,\n            headers=auth_hdr,\n            timeout=5,\n            allow_redirects=False  # Prevent open-redirect-based SSRF bypass\n        )\n        raw_resp.raise_for_status()\n        sensor_data = raw_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', 'metrics': sensor_data})"}