mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge commit from fork
- Normalize IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) to IPv4 before checking against blocked networks, preventing blocklist bypass - Add missing blocked ranges: CGNAT (100.64.0.0/10), IETF Protocol Assignments (192.0.0.0/24), Benchmarking (198.18.0.0/15) - Add comprehensive tests for IPv4-mapped bypass and new blocked ranges
This commit is contained in:
@@ -50,10 +50,13 @@ BLOCKED_IP_NETWORKS = [
|
||||
# IPv4 Ranges
|
||||
ipaddress.ip_network("0.0.0.0/8"), # "This" Network
|
||||
ipaddress.ip_network("10.0.0.0/8"), # Private-Use
|
||||
ipaddress.ip_network("100.64.0.0/10"), # Shared Address Space (CGNAT, RFC 6598)
|
||||
ipaddress.ip_network("127.0.0.0/8"), # Loopback
|
||||
ipaddress.ip_network("169.254.0.0/16"), # Link Local
|
||||
ipaddress.ip_network("172.16.0.0/12"), # Private-Use
|
||||
ipaddress.ip_network("192.0.0.0/24"), # IETF Protocol Assignments
|
||||
ipaddress.ip_network("192.168.0.0/16"), # Private-Use
|
||||
ipaddress.ip_network("198.18.0.0/15"), # Benchmarking
|
||||
ipaddress.ip_network("224.0.0.0/4"), # Multicast
|
||||
ipaddress.ip_network("240.0.0.0/4"), # Reserved for Future Use
|
||||
# IPv6 Ranges
|
||||
@@ -71,8 +74,17 @@ HOSTNAME_REGEX = re.compile(r"^[A-Za-z0-9.-]+$") # Basic DNS-safe hostname patt
|
||||
def _is_ip_blocked(ip: str) -> bool:
|
||||
"""
|
||||
Checks if the IP address is in a blocked network.
|
||||
|
||||
IPv4-mapped IPv6 addresses (e.g. ``::ffff:127.0.0.1``) are normalized to
|
||||
their IPv4 equivalent before checking, so the IPv4 blocklist cannot be
|
||||
bypassed by encoding a private IPv4 address as IPv6.
|
||||
"""
|
||||
ip_addr = ipaddress.ip_address(ip)
|
||||
|
||||
# Normalize IPv4-mapped IPv6 → IPv4 so the IPv4 blocklist applies
|
||||
if isinstance(ip_addr, ipaddress.IPv6Address) and ip_addr.ipv4_mapped:
|
||||
ip_addr = ip_addr.ipv4_mapped
|
||||
|
||||
return any(ip_addr in network for network in BLOCKED_IP_NETWORKS)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
from backend.util.request import pin_url, validate_url_host
|
||||
from backend.util.request import _is_ip_blocked, pin_url, validate_url_host
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -171,3 +171,76 @@ async def test_large_header_handling():
|
||||
|
||||
finally:
|
||||
await runner.cleanup()
|
||||
|
||||
|
||||
# ---------- IPv4-mapped IPv6 bypass tests (GHSA-8qc5-rhmg-r6r6) ----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ip, expected_blocked",
|
||||
[
|
||||
# IPv4-mapped IPv6 encoding of blocked IPv4 ranges
|
||||
("::ffff:127.0.0.1", True), # Loopback
|
||||
("::ffff:10.0.0.1", True), # Private-Use
|
||||
("::ffff:192.168.1.1", True), # Private-Use
|
||||
("::ffff:172.16.0.1", True), # Private-Use
|
||||
("::ffff:169.254.1.1", True), # Link Local
|
||||
("::ffff:100.64.0.1", True), # CGNAT (RFC 6598)
|
||||
# Plain IPv4 (should still be blocked)
|
||||
("127.0.0.1", True),
|
||||
("10.0.0.1", True),
|
||||
("100.64.0.1", True), # CGNAT
|
||||
("192.0.0.1", True), # IETF Protocol Assignments
|
||||
("198.18.0.1", True), # Benchmarking
|
||||
# Public IPs (should NOT be blocked)
|
||||
("8.8.8.8", False),
|
||||
("1.1.1.1", False),
|
||||
("::ffff:8.8.8.8", False), # IPv4-mapped but public
|
||||
# Native IPv6 blocked ranges
|
||||
("::1", True), # Loopback
|
||||
("fe80::1", True), # Link-local
|
||||
("fc00::1", True), # ULA
|
||||
# Public IPv6 (should NOT be blocked)
|
||||
("2607:f8b0:4004:800::200e", False), # Google
|
||||
],
|
||||
)
|
||||
def test_is_ip_blocked(ip: str, expected_blocked: bool):
|
||||
assert _is_ip_blocked(ip) == expected_blocked, (
|
||||
f"Expected _is_ip_blocked({ip!r}) == {expected_blocked}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw_url, resolved_ips, should_raise",
|
||||
[
|
||||
# IPv4-mapped IPv6 loopback — must be blocked
|
||||
("mapped-loopback.example.com", ["::ffff:127.0.0.1"], True),
|
||||
# IPv4-mapped IPv6 private — must be blocked
|
||||
("mapped-private.example.com", ["::ffff:10.0.0.1"], True),
|
||||
# CGNAT range — must be blocked
|
||||
("cgnat.example.com", ["100.64.0.1"], True),
|
||||
# Mixed: one public, one mapped-private — must be blocked (any blocked = reject)
|
||||
("mixed.example.com", ["8.8.8.8", "::ffff:192.168.1.1"], True),
|
||||
# All public — should pass
|
||||
("public.example.com", ["8.8.8.8", "9.9.9.9"], False),
|
||||
],
|
||||
)
|
||||
async def test_ipv4_mapped_ipv6_bypass(
|
||||
monkeypatch,
|
||||
raw_url: str,
|
||||
resolved_ips: list[str],
|
||||
should_raise: bool,
|
||||
):
|
||||
"""Ensures IPv4-mapped IPv6 addresses are checked against IPv4 blocklist."""
|
||||
|
||||
def mock_getaddrinfo(host, port, *args, **kwargs):
|
||||
return [(None, None, None, None, (ip, port)) for ip in resolved_ips]
|
||||
|
||||
monkeypatch.setattr("socket.getaddrinfo", mock_getaddrinfo)
|
||||
|
||||
if should_raise:
|
||||
with pytest.raises(ValueError):
|
||||
await validate_url_host(raw_url)
|
||||
else:
|
||||
url, _, ip_addresses = await validate_url_host(raw_url)
|
||||
assert ip_addresses # Should have resolved IPs
|
||||
|
||||
Reference in New Issue
Block a user