From c47d154051a76b275f24b2d37b3ccbc41697378d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 02:45:44 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20SSRF=20bypass=20via=20Local-Use=20IPv4/IPv6=20Translation?= =?UTF-8?q?=20(RFC=208215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added SSRF protection for Local-Use IPv4/IPv6 Translation mappings (`64:ff9b:1::/96`). Previously, Python's `ipaddress` module evaluated these addresses as globally routable IPv6, allowing an attacker to ping internal IPv4 addresses by wrapping them in this prefix. The embedded IPv4 payload is now manually unwrapped and validated against existing SSRF blocklists. Added corresponding unit tests to prevent regressions. Co-authored-by: ManupaKDU <95234271+ManupaKDU@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ test_testping1.py | 10 ++++++++++ testping1.py | 2 ++ 3 files changed, 17 insertions(+) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 010d6a4..c8888a6 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -85,3 +85,8 @@ **Vulnerability:** Attackers could bypass SSRF IP blocklists using SIIT (Stateless IP/ICMP Translation, RFC 2765) addresses. The format `::ffff:0:a.b.c.d` (using the `::ffff:0:0:0/96` prefix) evaluates as `is_global = True` in Python's `ipaddress` module and is NOT caught by the `ipv4_mapped` property. If an attacker passes such an address, the OS networking stack might route it directly to the embedded IPv4 target, bypassing internal security restrictions. **Learning:** Python's `ipaddress` module only natively extracts standard IPv4-mapped addresses (`::ffff:a.b.c.d`), failing to recognize or unwrap SIIT IPv4-translated addresses. **Prevention:** Always manually unwrap SIIT addresses by checking if the high 96 bits of the IPv6 integer match the SIIT prefix (`ip_int >> 32 == 0xffff0000`). If so, extract the underlying 32-bit IPv4 address using bitwise operations (`ip_int & 0xFFFFFFFF`) and validate it against the SSRF blocklist. + +## 2025-06-06 - SSRF bypass via Local-Use IPv4/IPv6 Translation (RFC 8215) +**Vulnerability:** Attackers could bypass SSRF IP blocklists using Local-Use IPv4/IPv6 Translation prefixes (e.g. `64:ff9b:1::/96`). This allowed attackers to ping local addresses using this prefix since the `ipaddress` module considers these to be `is_global = True` and does not automatically extract the embedded IPv4 address. +**Learning:** Python's `ipaddress` module does not intrinsically unwrap IPv4/IPv6 translated addresses like RFC 8215 mappings where the embedded IPv4 address acts as the suffix, causing them to be evaluated as fully routable global IPv6 addresses. +**Prevention:** Always manually unwrap the `/96` mapping by checking if the higher 96 bits match `0x0064ff9b0001000000000000` (`ip_int >> 32 == 0x0064ff9b0001000000000000`). If it matches, extract the underlying 32-bit IPv4 address with `ip_int & 0xFFFFFFFF` and run it through standard SSRF validations. diff --git a/test_testping1.py b/test_testping1.py index 17fd0a1..172b5be 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -79,6 +79,16 @@ 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_local_use_translation(self, mock_call): + """Test is_reachable prevents SSRF bypass via Local-Use IPv4/IPv6 Translation addresses.""" + ssrf_ips = ['64:ff9b:1::127.0.0.1', '64:ff9b:1::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_bypass_siit(self, mock_call): """Test is_reachable prevents SSRF bypass via SIIT (IPv4-translated) addresses.""" diff --git a/testping1.py b/testping1.py index fa70655..24399fd 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 (RFC 8215) + 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