fix(url_safety): block IPv4-mapped IPv6 addresses to prevent SSRF bypass

This commit is contained in:
Ryan Lee
2026-05-16 11:55:55 +08:00
committed by Teknium
parent e3f391c1ac
commit 6143ce1546
2 changed files with 88 additions and 0 deletions

View File

@@ -45,15 +45,26 @@ _BLOCKED_HOSTNAMES = frozenset({
# allow_private_urls toggle. These are cloud metadata / credential
# endpoints — the #1 SSRF target — and the link-local range where
# they all live.
#
# IPv4-mapped IPv6 variants are included because DNS resolvers may
# return ``::ffff:x.x.x.x`` for IPv4-only hosts, and Python's
# ipaddress module treats these as distinct from the plain IPv4
# address (they won't match ``ip in frozenset`` or ``ip in network``).
_ALWAYS_BLOCKED_IPS = frozenset({
ipaddress.ip_address("169.254.169.254"), # AWS/GCP/Azure/DO/Oracle metadata
ipaddress.ip_address("169.254.170.2"), # AWS ECS task metadata (task IAM creds)
ipaddress.ip_address("169.254.169.253"), # Azure IMDS wire server
ipaddress.ip_address("fd00:ec2::254"), # AWS metadata (IPv6)
ipaddress.ip_address("100.100.100.200"), # Alibaba Cloud metadata
# IPv4-mapped IPv6 variants — same endpoints reachable via ::ffff:x.x.x.x
ipaddress.ip_address("::ffff:169.254.169.254"),
ipaddress.ip_address("::ffff:169.254.170.2"),
ipaddress.ip_address("::ffff:169.254.169.253"),
ipaddress.ip_address("::ffff:100.100.100.200"),
})
_ALWAYS_BLOCKED_NETWORKS = (
ipaddress.ip_network("169.254.0.0/16"), # Entire link-local range (no legit agent target)
ipaddress.ip_network("::ffff:169.254.0.0/112"), # IPv4-mapped link-local range
)
# Exact HTTPS hostnames allowed to resolve to private/benchmark-space IPs.
@@ -137,6 +148,16 @@ def _reset_allow_private_cache() -> None:
def _is_blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
"""Return True if the IP should be blocked for SSRF protection."""
# IPv4-mapped IPv6 addresses (``::ffff:x.x.x.x``) should be checked
# by their embedded IPv4 address, not as IPv6
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped is not None:
embedded_ip = ip.ipv4_mapped
return (embedded_ip.is_private or embedded_ip.is_loopback or
embedded_ip.is_link_local or embedded_ip.is_reserved or
embedded_ip.is_multicast or embedded_ip.is_unspecified or
embedded_ip in _CGNAT_NETWORK)
# Standard IPv4/IPv6 address checking
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return True
if ip.is_multicast or ip.is_unspecified: