From 9947d441db313bff4847528dd747f32898ecc274 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:17:02 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20SSRF=20bypass=20via=20ISATAP=20tunneling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: The Python `ipaddress` module evaluates ISATAP addresses (e.g., `2001:db8::5efe:127.0.0.1`) as globally routable (`is_global=True`) and does not natively expose or validate the encapsulated IPv4 payload. This allows an attacker to bypass standard SSRF filters by wrapping private or loopback IPv4 addresses in an ISATAP IPv6 structure. 🎯 Impact: Attackers could scan and interact with internal network resources (like 127.0.0.1 or 192.168.x.x) that are meant to be protected by the SSRF filter. 🔧 Fix: Manually identify ISATAP identifiers (`0000:5EFE` or `0200:5EFE`) within the 64-bit interface identifier of an IPv6 address using bitwise operations, extract the embedded 32-bit IPv4 address, and validate it against the existing SSRF blocklist rules. ✅ Verification: Verified by running the unit test suite (`python3 -m unittest test_testping1.py`), which includes a new test `test_is_reachable_ssrf_bypass_isatap` that confirms ISATAP payloads are properly blocked. Co-authored-by: ManupaKDU <95234271+ManupaKDU@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ test_testping1.py | 12 ++++++++++++ testping1.py | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index d3e2b0a..90ea24d 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-30 - SSRF Bypass via ISATAP (RFC 5214) Tunneling Addresses +**Vulnerability:** Python's `ipaddress` module evaluates ISATAP addresses (e.g., `2001:db8::5efe:127.0.0.1`) as `is_global = True` and lacks a native `is_isatap` property. This allows attackers to bypass SSRF validations by embedding private or reserved IPv4 addresses inside ISATAP IPv6 structures. +**Learning:** `ipaddress` module does not intrinsically unwrap and validate all forms of IPv6 encapsulation. +**Prevention:** To prevent SSRF bypasses via ISATAP 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..a3fcd04 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -79,6 +79,18 @@ def test_is_reachable_ssrf_bypass_ipv4_mapped(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 tunneling addresses.""" + # 2001:db8::5efe:127.0.0.1 encapsulates 127.0.0.1 + # 2001:db8::200:5efe:192.168.1.1 encapsulates 192.168.1.1 + ssrf_ips = ['2001:db8::5efe:127.0.0.1', '2001:db8::200:5efe:192.168.1.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_cgnat_prevention(self, mock_call): """Test is_reachable prevents SSRF by rejecting Carrier-Grade NAT (CGNAT) IPs.""" diff --git a/testping1.py b/testping1.py index aa0ff6e..f0d5c7b 100644 --- a/testping1.py +++ b/testping1.py @@ -128,7 +128,7 @@ 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 + # 🛡️ 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 @@ -136,6 +136,10 @@ def is_reachable(ip, timeout=1): 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_id = (ip_int >> 32) & 0xFFFFFFFF + if isatap_id in (0x00005efe, 0x02005efe): # ISATAP tunnel + unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF) if unwrapped is not None: is_blocked = not unwrapped.is_global or unwrapped.is_multicast