Block NAT64 SSRF bypass (#4400)

This commit is contained in:
Jeremiah Lowin 2026-06-27 12:21:37 -04:00 committed by GitHub
commit 5de15e0c21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 31 additions and 0 deletions

View file

@ -22,6 +22,8 @@ from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
NAT64_WELL_KNOWN_PREFIX = ipaddress.ip_network("64:ff9b::/96")
def format_ip_for_url(ip_str: str) -> str:
"""Format IP address for use in URL (bracket IPv6 addresses).
@ -61,6 +63,7 @@ def is_ip_allowed(ip_str: str) -> bool:
- Link-local (169.254.x, fe80::) - includes AWS metadata!
- Reserved, unspecified
- RFC6598 Carrier-Grade NAT (100.64.0.0/10) - can point to internal networks
- NAT64 (64:ff9b::/96) - can point to internal networks
Additionally blocks multicast addresses (not caught by is_global).
@ -91,6 +94,8 @@ def is_ip_allowed(ip_str: str) -> bool:
if ip.teredo:
server, client = ip.teredo
return is_ip_allowed(str(server)) and is_ip_allowed(str(client))
if ip in NAT64_WELL_KNOWN_PREFIX:
return is_ip_allowed(str(ipaddress.IPv4Address(ip.packed[-4:])))
return True

View file

@ -51,6 +51,23 @@ class TestIsIPAllowed:
assert is_ip_allowed("::ffff:127.0.0.1") is False
assert is_ip_allowed("::ffff:192.168.1.1") is False
@pytest.mark.parametrize(
"address",
[
pytest.param("64:ff9b::7f00:1", id="loopback"),
pytest.param("64:ff9b::0a00:1", id="private"),
pytest.param("64:ff9b::a9fe:a9fe", id="link-local"),
pytest.param("64:ff9b::6440:1", id="cgnat"),
],
)
def test_nat64_ipv6_blocked_if_embedded_ipv4_blocked(self, address: str):
"""NAT64 IPv6 addresses should check the embedded IPv4."""
assert is_ip_allowed(address) is False
def test_nat64_ipv6_allowed_if_embedded_ipv4_allowed(self):
"""NAT64 IPv6 addresses should stay allowed for public embedded IPv4."""
assert is_ip_allowed("64:ff9b::0808:0808") is True
class TestValidateURL:
"""Tests for validate_url function."""
@ -83,6 +100,15 @@ class TestValidateURL:
with pytest.raises(SSRFError, match="blocked IP"):
await validate_url("https://example.com/path")
async def test_nat64_private_ip_rejected(self):
"""URLs resolving to NAT64-wrapped private IPs should be rejected."""
with patch(
"fastmcp.server.auth.ssrf.resolve_hostname",
return_value=["64:ff9b::0a00:1"],
):
with pytest.raises(SSRFError, match="blocked IP"):
await validate_url("https://example.com/path")
class TestSSRFSafeFetch:
"""Tests for ssrf_safe_fetch function."""