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

Language: Python
Severity: High
CWE: CWE-918

## Source
16

## Flow
16-4-7-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'}
    try:
        telemetry_url = f"{device_endpoint}/api/v2/telemetry/latest"
        response = requests.get(telemetry_url, headers=headers, timeout=5)
        if response.status_code == 200:
            return json.loads(response.text)
        return {'error': 'Device unreachable', 'status': response.status_code}
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

device_url = input("Enter IoT device URL: ")
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
data = fetch_iot_device_telemetry(device_url, token)
```

## Explanation

The application accepts an unsanitized URL from user input (line 16) and directly uses it to make an HTTP request (line 8) without any validation or allowlisting. This enables Server-Side Request Forgery (SSRF) attacks where an attacker can force the server to make requests to internal resources, cloud metadata endpoints, or arbitrary external systems.

## Remediation

The fix adds a comprehensive URL validation function that checks the scheme (HTTPS only), verifies the hostname against an allowlist of permitted IoT device domains, resolves the hostname to verify the IP address does not fall within private/internal/metadata IP ranges, and disables HTTP redirects to prevent redirect-based SSRF bypasses. This defense-in-depth approach prevents attackers from accessing internal services, cloud metadata endpoints, or arbitrary external systems.

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

# Allowlist of permitted device hostname patterns or specific hosts
ALLOWED_HOSTS_SUFFIX = ['.iot.company.com', '.devices.internal.company.com']
ALLOWED_SCHEMES = ['https']
BLOCKED_IP_RANGES = [
    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'),
]

def is_ip_blocked(ip_str):
    """Check if an IP address falls within blocked ranges."""
    try:
        ip = ipaddress.ip_address(ip_str)
        for network in BLOCKED_IP_RANGES:
            if ip in network:
                return True
        return False
    except ValueError:
        return True

def validate_device_url(device_endpoint):
    """Validate that the device URL is safe to connect to."""
    try:
        parsed = urlparse(device_endpoint)
    except Exception:
        return False, "Invalid URL format"

    # Check scheme
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False, f"Scheme '{parsed.scheme}' not allowed. Use HTTPS."

    # Check that hostname exists
    hostname = parsed.hostname
    if not hostname:
        return False, "No hostname provided"

    # Check against allowlisted host suffixes
    host_allowed = any(hostname.endswith(suffix) for suffix in ALLOWED_HOSTS_SUFFIX)
    if not host_allowed:
        return False, f"Host '{hostname}' is not in the allowed device hosts list"

    # Resolve hostname and check against blocked IP ranges
    try:
        resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443)
        for entry in resolved_ips:
            ip_str = entry[4][0]
            if is_ip_blocked(ip_str):
                return False, f"Resolved IP '{ip_str}' is in a blocked range"
    except socket.gaierror:
        return False, f"Unable to resolve hostname '{hostname}'"

    # Disallow path traversal or unexpected characters in the base URL
    if parsed.path and parsed.path != '/' and parsed.path != '':
        if '..' in parsed.path or '//' in parsed.path:
            return False, "Suspicious path detected in device URL"

    return True, "Valid"

def fetch_iot_device_telemetry(device_endpoint, auth_token):
    # Validate URL before making request
    is_valid, reason = validate_device_url(device_endpoint)
    if not is_valid:
        return {'error': f'Invalid device URL: {reason}'}

    headers = {'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json'}
    try:
        telemetry_url = f"{device_endpoint}/api/v2/telemetry/latest"
        response = requests.get(telemetry_url, headers=headers, timeout=5, allow_redirects=False)
        if response.status_code == 200:
            return json.loads(response.text)
        return {'error': 'Device unreachable', 'status': response.status_code}
    except requests.exceptions.RequestException as e:
        return {'error': str(e)}

device_url = input("Enter IoT device URL: ").strip()
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
data = fetch_iot_device_telemetry(device_url, token)
```
