fix(security): normalize IPv6-mapped IPv4 addresses in SSRF checks
::ffff:127.0.0.1 and ::ffff:169.254.169.254 are IPv6Address objects that match neither the IPv4 blocklists (127.0.0.0/8, 169.254.0.0/16) nor the IPv6 ones (::1/128), allowing SSRF bypass via DNS responses that return IPv6-mapped IPv4 addresses. Add _normalize_addr() to convert ipv4_mapped IPv6 addresses to their IPv4 form before blocklist/allowlist matching.
This commit is contained in:
@@ -36,10 +36,26 @@ def configure_ssrf_whitelist(cidrs: list[str]) -> None:
|
||||
_allowed_networks = nets
|
||||
|
||||
|
||||
def _normalize_addr(
|
||||
addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
||||
) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
|
||||
"""Normalize IPv6-mapped IPv4 addresses to their IPv4 form.
|
||||
|
||||
``::ffff:127.0.0.1`` is semantically identical to ``127.0.0.1`` but
|
||||
Python's ipaddress treats it as an IPv6Address that matches neither
|
||||
``127.0.0.0/8`` nor ``::1/128``. Converting it to IPv4 ensures
|
||||
blocklist/allowlist checks work correctly.
|
||||
"""
|
||||
if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:
|
||||
return addr.ipv4_mapped
|
||||
return addr
|
||||
|
||||
|
||||
def _is_private(addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
||||
if _allowed_networks and any(addr in net for net in _allowed_networks):
|
||||
normalized = _normalize_addr(addr)
|
||||
if _allowed_networks and any(normalized in net for net in _allowed_networks):
|
||||
return False
|
||||
return any(addr in net for net in _BLOCKED_NETWORKS)
|
||||
return any(normalized in net for net in _BLOCKED_NETWORKS)
|
||||
|
||||
|
||||
def validate_url_target(url: str, *, allow_loopback: bool = False) -> tuple[bool, str]:
|
||||
|
||||
Reference in New Issue
Block a user