mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
Block NAT64 SSRF bypass (#4400)
This commit is contained in:
parent
a1dd8b12e1
commit
5de15e0c21
2 changed files with 31 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue