app/services/ssrf.py (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
from __future__ import annotations
import ipaddress
from ipaddress import IPv4Address, IPv6Address
# AWS/GCP/Azure-style metadata endpoints commonly abused for SSRF
METADATA_HOSTNAMES = frozenset(
{
"metadata.google.internal",
"metadata.goog",
"kubernetes.default",
"kubernetes.default.svc",
}
)
def is_blocked_hostname(hostname: str) -> bool:
host = hostname.strip(".").lower()
if host in METADATA_HOSTNAMES:
return True
if host.endswith(".local") or host.endswith(".localhost"):
return True
return host in {"localhost", "metadata"}
def is_public_ip(ip_str: str) -> bool:
"""Return True if the IP is globally routable / safe to connect to."""
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
return False
return not _is_blocked_ip(ip)
def _is_blocked_ip(ip: IPv4Address | IPv6Address) -> bool:
if ip.is_loopback:
return True
if ip.is_private:
return True
if ip.is_link_local:
return True
if ip.is_multicast:
return True
if ip.is_reserved:
return True
if ip.is_unspecified:
return True
# CGNAT / documentation / benchmarking often flagged via is_private or reserved;
# also block classic metadata IPv4 explicitly.
if isinstance(ip, IPv4Address):
if ip == IPv4Address("169.254.169.254"):
return True
if ip in ipaddress.ip_network("0.0.0.0/8"):
return True
if isinstance(ip, IPv6Address):
# Unique local addresses
if ip.ipv4_mapped is not None:
return _is_blocked_ip(ip.ipv4_mapped)
if (int(ip) >> 118) == 0b1111110: # fc00::/7
return True
return False
def assert_safe_destination(hostname: str, resolved_ips: list[str]) -> None:
"""Raise ValueError if hostname or any resolved IP is unsafe."""
if is_blocked_hostname(hostname):
raise ValueError(f"blocked hostname: {hostname}")
if not resolved_ips:
raise ValueError("no resolved addresses")
for ip in resolved_ips:
if not is_public_ip(ip):
raise ValueError(f"blocked address: {ip}")
def assert_connected_ip_safe(ip_str: str) -> None:
"""Re-check the address actually used for the connection (anti-rebinding)."""
if not is_public_ip(ip_str):
raise ValueError(f"blocked connected address: {ip_str}")
|