# SSRF via User-Controlled URL in requests.get()

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('device_token', 'default-token')
    headers = {'Authorization': f'Bearer {auth_token}', 'User-Agent': 'IoT-Gateway/2.1'}
    telemetry_response = requests.get(device_endpoint, headers=headers, timeout=10)
    return jsonify({'status': 'success', 'data': telemetry_response.json(), 'code': telemetry_response.status_code})
```

## Explanation

The application accepts a user-controlled 'telemetry_url' parameter without validation and directly passes it to requests.get(). This allows attackers to perform Server-Side Request Forgery (SSRF) attacks, potentially accessing internal services, cloud metadata endpoints, or performing port scanning on internal networks.

## Remediation

The fix implements a comprehensive URL validation function that checks the scheme (only HTTPS allowed), restricts allowed ports, resolves the hostname to IP addresses, and verifies none of them belong to private/internal/link-local networks. Additionally, redirects are disabled to prevent redirect-based SSRF bypasses, and proper error handling is added.

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

app = Flask(__name__)

ALLOWED_SCHEMES = {'https'}
ALLOWED_PORTS = {443, 8443, 8883}
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('100.64.0.0/10'),
    ipaddress.ip_network('198.18.0.0/15'),
    ipaddress.ip_network('::1/128'),
    ipaddress.ip_network('fc00::/7'),
    ipaddress.ip_network('fe80::/10'),
]


def is_safe_url(url):
    """Validate that the URL is safe to request (not targeting internal resources)."""
    if not url or not isinstance(url, str):
        return False, "Invalid URL provided"

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

    if parsed.scheme not in ALLOWED_SCHEMES:
        return False, f"Scheme '{parsed.scheme}' not allowed. Only {ALLOWED_SCHEMES} 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} not allowed. Only {ALLOWED_PORTS} permitted."

    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, "Access to internal/private network addresses is forbidden"

    return True, None


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

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

    is_safe, error_message = is_safe_url(device_endpoint)
    if not is_safe:
        return jsonify({'status': 'error', 'message': error_message}), 400

    headers = {'Authorization': f'Bearer {auth_token}', 'User-Agent': 'IoT-Gateway/2.1'}

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