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

@@ -482,3 +482,70 @@ class TestIsAlwaysBlockedUrl:
"""security.allow_private_urls can NOT unblock cloud metadata."""
monkeypatch.setenv("HERMES_ALLOW_PRIVATE_URLS", "true")
assert is_always_blocked_url("http://169.254.169.254/") is True
class TestIPv4MappedIPv6SSRF:
"""Regression tests for SSRF bypass via IPv4-mapped IPv6 addresses.
DNS resolvers may return ``::ffff:x.x.x.x`` for IPv4-only hosts.
Python's ipaddress module treats these as distinct from the plain
IPv4 address, so ``ip in frozenset({IPv4Address(...)})`` and
``ip in IPv4Network(...)`` both return False. Without explicit
handling, an attacker could use IPv4-mapped addresses to bypass
all SSRF protections.
"""
# ── _is_blocked_ip direct tests ──
@pytest.mark.parametrize("ip_str", [
"::ffff:100.64.0.1", # CGNAT start
"::ffff:100.100.100.200", # Alibaba Cloud metadata (in CGNAT range)
"::ffff:100.127.255.254", # CGNAT end
"::ffff:169.254.42.99", # Link-local (non-metadata)
"::ffff:0.0.0.0", # Unspecified
"::ffff:224.0.0.1", # Multicast
])
def test_ipv4_mapped_blocked_ips(self, ip_str):
"""IPv4-mapped IPv6 addresses that should be blocked."""
ip = ipaddress.ip_address(ip_str)
assert _is_blocked_ip(ip) is True, f"{ip_str} should be blocked"
@pytest.mark.parametrize("ip_str", [
"::ffff:8.8.8.8", # Public DNS
"::ffff:93.184.216.34", # example.com
"::ffff:100.0.0.1", # Not in CGNAT range
])
def test_ipv4_mapped_allowed_ips(self, ip_str):
"""IPv4-mapped IPv6 addresses that should be allowed."""
ip = ipaddress.ip_address(ip_str)
assert _is_blocked_ip(ip) is False, f"{ip_str} should be allowed"
# ── is_safe_url integration tests: always-blocked metadata IPs ──
def test_ipv4_mapped_aws_metadata_blocked(self):
"""::ffff:169.254.169.254 (AWS metadata) must always be blocked."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::ffff:169.254.169.254", 0, 0, 0)),
]):
assert is_safe_url("http://aws-metadata.internal/") is False
def test_ipv4_mapped_ecs_metadata_blocked(self):
"""::ffff:169.254.170.2 (AWS ECS task metadata) must always be blocked."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::ffff:169.254.170.2", 0, 0, 0)),
]):
assert is_safe_url("http://ecs-metadata.internal/") is False
def test_ipv4_mapped_azure_wire_server_blocked(self):
"""::ffff:169.254.169.253 (Azure IMDS wire server) must always be blocked."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::ffff:169.254.169.253", 0, 0, 0)),
]):
assert is_safe_url("http://azure-metadata.internal/") is False
def test_ipv4_mapped_alibaba_metadata_blocked(self):
"""::ffff:100.100.100.200 (Alibaba Cloud metadata) must always be blocked."""
with patch("socket.getaddrinfo", return_value=[
(10, 1, 6, "", ("::ffff:100.100.100.200", 0, 0, 0)),
]):
assert is_safe_url("http://aliyun-metadata.internal/") is False