diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 010d6a4..87ccd3c 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -63,6 +63,10 @@ **Vulnerability:** Server-Side Request Forgery (SSRF) bypass allowing attackers to ping internal IP addresses by wrapping them in IPv6 NAT64 (64:ff9b::/96) or IPv4-compatible (::/96) formats, or by using deprecated site-local addresses (fec0::/10). **Learning:** Python's `ipaddress` module does not automatically apply `is_private` or SSRF checks to the embedded IPv4 payloads of NAT64 or IPv4-compatible addresses. Additionally, it considers deprecated site-local addresses as globally routable (`is_private` returns False, `is_global` returns True), requiring a specific check using `is_site_local`. **Prevention:** Always manually unpack and validate the embedded IPv4 payloads for NAT64 and IPv4-compatible IPv6 addresses using bitwise integer operations. For site-local addresses, explicitly check `getattr(ip_obj, 'is_site_local', False)` to ensure backwards-compatibility across Python versions while blocking the deprecated internal range. +## 2024-05-31 - Fix SSRF Bypass via Local-Use IPv4/IPv6 Translation (RFC 8215) +**Vulnerability:** Server-Side Request Forgery (SSRF) bypass allowing attackers to ping internal IP addresses by wrapping them in Local-Use IPv4/IPv6 Translation formats (RFC 8215, 64:ff9b:1::/96). +**Learning:** Python's `ipaddress` module does not natively unwrap these embedded IPv4 addresses. Similar to NAT64, it evaluates these addresses as `is_global = False` but `is_private = True`, which can lead to bypass if only `is_global` check is present and unwrapping logic is missing. +**Prevention:** To prevent SSRF bypasses via Local-Use IPv4/IPv6 Translation addresses, manually unwrap the embedded IPv4 address by checking if the high 96 bits match the prefix (`ip_int >> 32 == 0x0064ff9b0001000000000000`) and extract the underlying 32-bit IPv4 address using bitwise operations (`ip_int & 0xFFFFFFFF`) to validate it against SSRF rules. ## 2025-02-28 - SSRF Bypass via Carrier-Grade NAT (CGNAT) **Vulnerability:** The Python `ipaddress` module's `is_private` property evaluates Carrier-Grade NAT (CGNAT) addresses (e.g., `100.64.0.0/10`) and benchmark ranges as `False`. This allows attackers to bypass standard SSRF filters and potentially scan internal shared infrastructure if the hosting environment natively routes those non-public ranges. **Learning:** `is_private` is not a catch-all for internal or non-routable IPs. The module maintains a separate `is_global` property that more accurately defines publicly routable internet addresses. diff --git a/test_testping1.py b/test_testping1.py index 17fd0a1..5b592b8 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -319,8 +319,12 @@ def test_is_reachable_ssrf_bypass_teredo(self, mock_call): @patch('testping1.subprocess.call') def test_is_reachable_ssrf_bypass_nat64_and_compat(self, mock_call): - """Test is_reachable prevents SSRF bypass via NAT64 and IPv4-compatible addresses.""" - ssrf_ips = ['64:ff9b::127.0.0.1', '64:ff9b::192.168.1.1', '::127.0.0.1', '::192.168.1.1'] + """Test is_reachable prevents SSRF bypass via NAT64, Local-Use Translation (RFC 8215) and IPv4-compatible addresses.""" + ssrf_ips = [ + '64:ff9b::127.0.0.1', '64:ff9b::192.168.1.1', # NAT64 + '64:ff9b:1::127.0.0.1', '64:ff9b:1::192.168.1.1', # Local-Use Translation (RFC 8215) + '::127.0.0.1', '::192.168.1.1' # IPv4-compatible + ] for ip in ssrf_ips: with self.assertLogs(level='ERROR') as log: self.assertFalse(is_reachable(ip)) diff --git a/testping1.py b/testping1.py index fa70655..f1d1a01 100644 --- a/testping1.py +++ b/testping1.py @@ -148,6 +148,8 @@ def is_reachable(ip, timeout=1): unwrapped = None if ip_int >> 32 == 0x0064ff9b0000000000000000: # NAT64 64:ff9b::/96 unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) + elif ip_int >> 32 == 0x0064ff9b0001000000000000: # Local-Use IPv4/IPv6 Translation 64:ff9b:1::/96 + unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) elif ip_int >> 32 == 0xffff0000: # SIIT (IPv4-translated) ::ffff:0:a.b.c.d unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) elif ip_int < 2**32 and ip_int not in (0, 1): # IPv4-compatible ::w.x.y.z