From a34e1df6fc2b1d195bf9a17a7e1e1eea8257d080 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:27:00 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20SSRF=20bypass=20via=20IPv4-mapped=20IPv6=20addresses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add check to unwrap `ipv4_mapped` IPv6 addresses before evaluating SSRF protection properties. - Add unit tests for IPv4-mapped loopback, link-local, unspecified, multicast, and reserved addresses to prevent regressions. Co-authored-by: ManupaKDU <95234271+ManupaKDU@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ test_testping1.py | 10 ++++++++++ testping1.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index b733aec..1f5d071 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -36,3 +36,7 @@ **Vulnerability:** Denial of Service (DoS) via Application Crash. **Learning:** Python 3.11+ introduced `sys.set_int_max_str_digits` which limits the conversion between massive integers and strings (e.g., calling `repr()` on an int like `10**100000`). When untrusted large integers are passed as arguments (like `ip` or `timeout`) and later sanitized for logging via `repr()`, it raises an unhandled `ValueError` that bypasses standard exception handlers and crashes the entire worker thread pool. **Prevention:** To prevent thread exhaustion and DoS, always explicitly enforce boundary checks (`type(var) is int` and size limits) on arbitrary inputs *before* any string formatting or `repr()` usage. As a defense in depth measure, wrap explicit `repr()` calls on untrusted dynamic inputs in a `try...except ValueError` block to provide a safe fallback string like ``. +## 2024-05-18 - SSRF Bypass via IPv4-mapped IPv6 Addresses +**Vulnerability:** Attackers could bypass SSRF IP blocklists (e.g., checking `is_link_local` to block 169.254.169.254) by passing the equivalent IPv4-mapped IPv6 address (e.g., `::ffff:169.254.169.254`). +**Learning:** Python's `ipaddress` module does not apply all IPv4 boolean property checks to IPv4-mapped IPv6 objects. For example, `is_link_local` and `is_unspecified` return `False` for their mapped equivalents, allowing malicious inputs to bypass validation while the OS networking stack natively routes the packet to the IPv4 target. +**Prevention:** To prevent SSRF bypasses via IPv4-mapped IPv6 addresses, explicitly unwrap the mapped IPv4 address using `getattr(ip_obj, 'ipv4_mapped', None)` and apply security validation checks directly to the underlying `IPv4Address` object if it exists. diff --git a/test_testping1.py b/test_testping1.py index 7e7f232..4aa6f9b 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -53,6 +53,16 @@ def test_is_reachable_type_error(self, mock_call): self.assertIn(f"Invalid IP address format: {repr(invalid_ip)}", log.output[0]) mock_call.assert_not_called() + @patch('testping1.subprocess.call') + def test_is_reachable_ssrf_bypass_ipv4_mapped(self, mock_call): + """Test is_reachable prevents SSRF bypass via IPv4-mapped IPv6 addresses.""" + ssrf_mapped_ips = ['::ffff:127.0.0.1', '::ffff:169.254.169.254', '::ffff:224.0.0.1', '::ffff:0.0.0.0', '::ffff:255.255.255.255'] + for ip in ssrf_mapped_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_argument_injection(self, mock_call): """Test is_reachable prevents argument injection by rejecting invalid IPs.""" diff --git a/testping1.py b/testping1.py index f6c3628..14dba45 100644 --- a/testping1.py +++ b/testping1.py @@ -89,7 +89,17 @@ def is_reachable(ip, timeout=1): # 🛡️ Sentinel: Prevent Server-Side Request Forgery (SSRF) # Block loopback, link-local, multicast, unspecified, and reserved addresses from being pinged. # reserved addresses include the broadcast address (255.255.255.255) - if ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_multicast or ip_obj.is_unspecified or ip_obj.is_reserved: + + # 🛡️ Sentinel: Prevent SSRF bypass via IPv4-mapped IPv6 addresses. + # Python's ipaddress module does not apply all IPv4 property checks (like + # is_link_local or is_unspecified) to IPv4-mapped IPv6 addresses (e.g., ::ffff:169.254.169.254). + # We must unwrap the IPv4 address before validating it against the blocklist. + ip_to_check = ip_obj + mapped_ip = getattr(ip_obj, 'ipv4_mapped', None) + if mapped_ip is not None: + ip_to_check = mapped_ip + + if ip_to_check.is_loopback or ip_to_check.is_link_local or ip_to_check.is_multicast or ip_to_check.is_unspecified or ip_to_check.is_reserved: # 🛡️ Sentinel: Sanitize log input using repr() to prevent CRLF/Log Injection # IPv6 addresses can contain an arbitrary scope ID (e.g., %eth0\r\n) which is # not sanitized by ipaddress.ip_address() and could allow log spoofing.