SSRF-by-DNS: every answer must be public
A scanner is the ideal SSRF pivot: point it at a public-looking name that resolves to 169.254.169.254 and it fetches cloud-metadata credentials for you. The guard resolves the name and requires that EVERY answer is globally public. Requiring all of them, not just one, is the point: it defeats a DNS-rebinding answer that mixes a public and a private record. This is the app-layer half; a kernel egress firewall is the second.
def resolve_safe(host: str) -> list[str]:
"""Resolve host and assert EVERY answer is public. Returns the pinned IPs."""
infos = socket.getaddrinfo(host, None)
ips = sorted({cast("str", i[4][0]) for i in infos})
if not ips:
raise UnsafeTargetError("does not resolve")
for ip in ips:
if not _ip_is_public(ip):
raise UnsafeTargetError(f"{host} resolves to non-public address {ip}")
return ips
def _ip_is_public(ip: str) -> bool:
addr = ipaddress.ip_address(ip)
if not addr.is_global: # private / reserved / loopback / multicast
return False
# _BLOCKED is an explicit second pass, so one missed is_global flag
# can't open an internal range (incl. IPv4-mapped IPv6).
return not any(addr in net for net in _BLOCKED)