diff --git a/.jules/sentinel.md b/.jules/sentinel.md index d3e2b0a..35d589d 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -75,3 +75,8 @@ **Vulnerability:** Python's `ipaddress.ip_address()` function accepts both `str` and `bytes`. When passing an extremely large bytes object (e.g., `b"A" * 10**8`), the module can take several seconds to raise a `ValueError` due to inefficient internal parsing logic, leading to a CPU exhaustion Denial of Service (DoS). **Learning:** Checking the length of `str` inputs before passing them to `ipaddress.ip_address()` is not sufficient to prevent DoS, as an attacker could pass a massive `bytes` object if the function accepts polymorphic types. **Prevention:** Always enforce strict length limits (e.g., <= 100 characters/bytes) on *both* `str` and `bytes` inputs before attempting to parse them using the `ipaddress` module. Use `isinstance(ip, (str, bytes))` and check `len()`. + +## 2024-05-11 - SSRF Bypass via ISATAP IPv6 Addresses +**Vulnerability:** The application was vulnerable to an SSRF bypass when passed ISATAP tunneling addresses (e.g., `2001:db8::5efe:127.0.0.1` or `2001:db8::200:5efe:127.0.0.1`), which encapsulate a blocked internal IPv4 address. +**Learning:** Python's `ipaddress` module evaluates these addresses as `is_global = True` and does not provide an `isatap` property to easily extract the embedded IPv4 address. +**Prevention:** To prevent SSRF bypasses via ISATAP (RFC 5214) tunneling addresses, manually identify and unwrap the embedded IPv4 address. Extract the 32-bit ISATAP identifier using `(ip_int >> 32) & 0xFFFFFFFF` and check for `0x00005efe` or `0x02005efe`, then validate the underlying IPv4 address against SSRF rules. diff --git a/test_testping1.py b/test_testping1.py index 9f39c61..d6f136c 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -281,6 +281,20 @@ def test_is_reachable_ssrf_bypass_teredo(self, mock_call): self.assertIn("IP address not allowed for scanning", log.output[0]) mock_call.assert_not_called() + @patch('testping1.subprocess.call') + def test_is_reachable_ssrf_bypass_isatap(self, mock_call): + """Test is_reachable prevents SSRF bypass via ISATAP addresses.""" + ssrf_ips = [ + '2001:db8::5efe:127.0.0.1', + 'fe80::5efe:192.168.1.1', + '2001:db8::200:5efe:127.0.0.1' + ] + for ip in ssrf_ips: + with self.assertLogs(level='ERROR') as log: + self.assertFalse(is_reachable(ip)) + self.assertIn("IP address not allowed for scanning", log.output[0]) + mock_call.assert_not_called() + @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.""" diff --git a/testping1.py b/testping1.py index aa0ff6e..941e600 100644 --- a/testping1.py +++ b/testping1.py @@ -128,14 +128,18 @@ def is_reachable(ip, timeout=1): not t_cli.is_global or t_cli.is_multicast ) else: - # 🛡️ Sentinel: Unpack NAT64 (RFC 6052) and IPv4-compatible (RFC 4291) addresses manually - # as Python's ipaddress module does not natively unwrap them for SSRF checks. + # 🛡️ Sentinel: Unpack NAT64 (RFC 6052), IPv4-compatible (RFC 4291), and ISATAP (RFC 5214) + # addresses manually as Python's ipaddress module does not natively unwrap them for SSRF checks. ip_int = int(ip_obj) unwrapped = None if ip_int >> 32 == 0x0064ff9b0000000000000000: # NAT64 64:ff9b::/96 unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) elif ip_int < 2**32 and ip_int not in (0, 1): # IPv4-compatible ::w.x.y.z unwrapped = ipaddress.IPv4Address(ip_int) + else: + isatap_identifier = (ip_int >> 32) & 0xFFFFFFFF + if isatap_identifier == 0x00005efe or isatap_identifier == 0x02005efe: # ISATAP + unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) if unwrapped is not None: is_blocked = not unwrapped.is_global or unwrapped.is_multicast