# SSRF via Unvalidated requests.get() URL Fetching

Language: Python
Severity: High
CWE: CWE-918

## Source
8

## Flow
8-10

## Sink
10

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

app = Flask(__name__)

@app.route('/api/iot/device/health', methods=['POST'])
def check_device_health():
    device_endpoint = request.json.get('callback_url')
    device_id = request.json.get('device_id', 'unknown')
    try:
        health_response = requests.get(device_endpoint, timeout=5)
        return jsonify({'device_id': device_id, 'status': health_response.status_code, 'data': health_response.text[:200]})
    except requests.RequestException as e:
        return jsonify({'device_id': device_id, 'error': str(e)}), 500
```

## Explanation

The application accepts a user-controlled 'callback_url' from the JSON request body without any validation and directly passes it to requests.get(). This allows an attacker to force the server to make HTTP requests to arbitrary internal or external URLs, enabling Server-Side Request Forgery (SSRF) attacks against internal infrastructure or metadata endpoints.

## Remediation

The fix introduces a URL validation function that parses the callback URL, ensures only http/https schemes are allowed, resolves the hostname to its IP addresses, and checks each resolved IP against a blocklist of private/internal networks (including the cloud metadata endpoint 169.254.169.254). Additionally, redirects are disabled to prevent bypass via open redirects, and URLs with embedded credentials are rejected.

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

app = Flask(__name__)

ALLOWED_SCHEMES = {'http', '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'),
]


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

    if not parsed.hostname:
        return False

    if parsed.scheme not in ALLOWED_SCHEMES:
        return False

    if parsed.username or parsed.password:
        return False

    try:
        addrinfos = socket.getaddrinfo(parsed.hostname, parsed.port or 80)
    except socket.gaierror:
        return False

    for addrinfo in addrinfos:
        ip = ipaddress.ip_address(addrinfo[4][0])
        for network in BLOCKED_NETWORKS:
            if ip in network:
                return False

    return True


@app.route('/api/iot/device/health', methods=['POST'])
def check_device_health():
    device_endpoint = request.json.get('callback_url')
    device_id = request.json.get('device_id', 'unknown')

    if not device_endpoint or not isinstance(device_endpoint, str):
        return jsonify({'device_id': device_id, 'error': 'Missing or invalid callback_url'}), 400

    if not is_safe_url(device_endpoint):
        return jsonify({'device_id': device_id, 'error': 'Invalid or disallowed callback URL'}), 403

    try:
        health_response = requests.get(device_endpoint, timeout=5, allow_redirects=False)
        return jsonify({'device_id': device_id, 'status': health_response.status_code, 'data': health_response.text[:200]})
    except requests.RequestException as e:
        return jsonify({'device_id': device_id, 'error': str(e)}), 500
```
