# Server-Side Request Forgery via requests.get on User-Controlled URL

Language: Python
Severity: High
CWE: CWE-918

## Source
4

## Flow
4-6-8

## Sink
8

## Vulnerable Code
```python
import requests
import json

def fetch_iot_device_telemetry(device_endpoint, auth_token):
    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}
    telemetry_url = f"{device_endpoint}/api/v2/telemetry/current"
    try:
        response = requests.get(telemetry_url, headers=headers, timeout=5)
        if response.status_code == 200:
            return {'status': 'success', 'data': response.json(), 'source': telemetry_url}
        return {'status': 'error', 'message': 'Device unreachable', 'code': response.status_code}
    except requests.exceptions.RequestException as e:
        return {'status': 'error', 'message': str(e)}
```

## Explanation

The function accepts a user-controlled 'device_endpoint' parameter without validation and directly uses it to construct a URL that is passed to requests.get(). This allows an attacker to make the server send HTTP requests to arbitrary internal or external systems, enabling Server-Side Request Forgery (SSRF) attacks to scan internal networks, access cloud metadata services, or exfiltrate data.

## Remediation

The fix adds comprehensive URL validation before making the HTTP request. It restricts the scheme to HTTPS only, resolves the hostname to check that all resolved IP addresses are not in private/internal network ranges (blocking access to cloud metadata services, localhost, and internal networks), restricts allowed ports, disables redirects to prevent redirect-based SSRF bypasses, and removes the source URL and raw exception details from responses to avoid information leakage.

## Secure Code
```python
import requests
import json
import ipaddress
from urllib.parse import urlparse
import socket

ALLOWED_SCHEMES = {'https'}
BLOCKED_NETWORKS = [
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('169.254.0.0/16'),
    ipaddress.ip_network('0.0.0.0/8'),
    ipaddress.ip_network('::1/128'),
    ipaddress.ip_network('fc00::/7'),
    ipaddress.ip_network('fe80::/10'),
]

ALLOWED_PORTS = {443, 8443}


def is_safe_url(url):
    """Validate that the URL does not point to internal/private resources."""
    try:
        parsed = urlparse(url)
    except ValueError:
        return False, "Invalid URL format"

    if parsed.scheme not in ALLOWED_SCHEMES:
        return False, f"Scheme '{parsed.scheme}' is not allowed. Only HTTPS is permitted."

    hostname = parsed.hostname
    if not hostname:
        return False, "No hostname found in URL"

    port = parsed.port or (443 if parsed.scheme == 'https' else 80)
    if port not in ALLOWED_PORTS:
        return False, f"Port {port} is not allowed"

    try:
        addrinfos = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)
    except socket.gaierror:
        return False, "Unable to resolve hostname"

    for addrinfo in addrinfos:
        ip = ipaddress.ip_address(addrinfo[4][0])
        for blocked_network in BLOCKED_NETWORKS:
            if ip in blocked_network:
                return False, f"Access to private/internal network addresses is not allowed"

    return True, None


def fetch_iot_device_telemetry(device_endpoint, auth_token):
    """Retrieve real-time telemetry data from a validated IoT device endpoint."""
    device_endpoint = device_endpoint.rstrip('/')
    telemetry_url = f"{device_endpoint}/api/v2/telemetry/current"

    is_safe, error_message = is_safe_url(telemetry_url)
    if not is_safe:
        return {'status': 'error', 'message': f'Invalid device endpoint: {error_message}'}

    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}
    try:
        response = requests.get(
            telemetry_url,
            headers=headers,
            timeout=5,
            allow_redirects=False
        )
        if response.status_code == 200:
            return {'status': 'success', 'data': response.json()}
        return {'status': 'error', 'message': 'Device unreachable', 'code': response.status_code}
    except requests.exceptions.RequestException as e:
        return {'status': 'error', 'message': 'Request failed'}
```
