# SSRF via requests.get() on Untrusted URL

Language: Python
Severity: High
CWE: CWE-918

## Source
8

## Flow
8-11

## Sink
11

## Vulnerable Code
```python
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/iot/device-status', methods=['POST'])
def fetch_device_telemetry():
    device_endpoint = request.json.get('telemetry_url')
    auth_token = request.json.get('token', 'default-key')
    headers = {'Authorization': f'Bearer {auth_token}'}
    response = requests.get(device_endpoint, headers=headers, timeout=5)
    return jsonify({'status': 'success', 'data': response.json(), 'code': response.status_code})
```

## Explanation

The application accepts a user-controlled 'telemetry_url' parameter without validation and directly passes it to requests.get(). This enables Server-Side Request Forgery (SSRF) attacks where attackers can force the server to make requests to internal resources, cloud metadata services (e.g., http://169.254.169.254/latest/meta-data/), or other restricted endpoints that should not be accessible externally.

## Remediation

The fix introduces a URL validation function that enforces an allowlist of permitted IoT device domains, blocks requests to private/internal IP ranges (including cloud metadata endpoints like 169.254.169.254), only permits HTTP/HTTPS schemes, performs DNS resolution to detect DNS rebinding attacks targeting internal IPs, and disables redirects to prevent redirect-based SSRF bypasses.

## Secure Code
```python
import requests
from flask import Flask, request, jsonify
from urllib.parse import urlparse
import ipaddress
import socket

app = Flask(__name__)

# Allowlist of permitted device endpoint hostnames/domains
ALLOWED_DOMAINS = [
    '.device.iot.internal',
    '.iot-telemetry.example.com',
]

# Blocked IP ranges (internal/private networks and link-local)
BLOCKED_NETWORKS = [
    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('127.0.0.0/8'),
    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_valid_telemetry_url(url):
    """Validate that the URL is safe to request (not targeting internal resources)."""
    if not url:
        return False, "Missing URL"

    try:
        parsed = urlparse(url)
    except Exception:
        return False, "Invalid URL format"

    # Only allow http and https schemes
    if parsed.scheme not in ('http', 'https'):
        return False, "Only HTTP and HTTPS schemes are allowed"

    # Ensure hostname is present
    hostname = parsed.hostname
    if not hostname:
        return False, "Missing hostname"

    # Check against allowed domains
    domain_allowed = any(hostname.endswith(domain) for domain in ALLOWED_DOMAINS)
    if not domain_allowed:
        return False, f"Domain '{hostname}' is not in the allowlist of permitted IoT device endpoints"

    # Resolve the hostname and check against blocked IP ranges
    try:
        resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443, proto=socket.IPPROTO_TCP)
    except socket.gaierror:
        return False, "Unable to resolve hostname"

    for result in resolved_ips:
        ip_str = result[4][0]
        try:
            ip = ipaddress.ip_address(ip_str)
            for blocked_net in BLOCKED_NETWORKS:
                if ip in blocked_net:
                    return False, f"Resolved IP {ip_str} is in a blocked network range"
        except ValueError:
            return False, "Invalid resolved IP address"

    return True, None


@app.route('/api/iot/device-status', methods=['POST'])
def fetch_device_telemetry():
    if not request.json:
        return jsonify({'status': 'error', 'message': 'Request body must be JSON'}), 400

    device_endpoint = request.json.get('telemetry_url')
    auth_token = request.json.get('token', 'default-key')

    # Validate the URL before making the request
    is_valid, error_message = is_valid_telemetry_url(device_endpoint)
    if not is_valid:
        return jsonify({'status': 'error', 'message': f'Invalid telemetry URL: {error_message}'}), 400

    headers = {'Authorization': f'Bearer {auth_token}'}

    try:
        response = requests.get(device_endpoint, headers=headers, timeout=5, allow_redirects=False)
        return jsonify({'status': 'success', 'data': response.json(), 'code': response.status_code})
    except requests.exceptions.RequestException as e:
        return jsonify({'status': 'error', 'message': f'Failed to reach device: {str(e)}'}), 502
```
