fix(url_safety): block IPv4-mapped IPv6 addresses to prevent SSRF bypass
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user