Skip to content

Commit b20f6e5

Browse files
Merge pull request #56 from Zektopic/sentinel/fix-ssrf-bypass-ipv4-mapped-1602020334448431204
🛡️ Sentinel: [CRITICAL] Fix SSRF bypass via IPv4-mapped IPv6 addresses
2 parents e0fab63 + a34e1df commit b20f6e5

3 files changed

Lines changed: 25 additions & 1 deletion

File tree

.jules/sentinel.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@
3636
**Vulnerability:** Denial of Service (DoS) via Application Crash.
3737
**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.
3838
**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 `<unrepresentable>`.
39+
## 2024-05-18 - SSRF Bypass via IPv4-mapped IPv6 Addresses
40+
**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`).
41+
**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.
42+
**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.

test_testping1.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ def test_is_reachable_type_error(self, mock_call):
5353
self.assertIn(f"Invalid IP address format: {repr(invalid_ip)}", log.output[0])
5454
mock_call.assert_not_called()
5555

56+
@patch('testping1.subprocess.call')
57+
def test_is_reachable_ssrf_bypass_ipv4_mapped(self, mock_call):
58+
"""Test is_reachable prevents SSRF bypass via IPv4-mapped IPv6 addresses."""
59+
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']
60+
for ip in ssrf_mapped_ips:
61+
with self.assertLogs(level='ERROR') as log:
62+
self.assertFalse(is_reachable(ip))
63+
self.assertIn("IP address not allowed for scanning", log.output[0])
64+
mock_call.assert_not_called()
65+
5666
@patch('testping1.subprocess.call')
5767
def test_is_reachable_argument_injection(self, mock_call):
5868
"""Test is_reachable prevents argument injection by rejecting invalid IPs."""

testping1.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ def is_reachable(ip, timeout=1):
8989
# 🛡️ Sentinel: Prevent Server-Side Request Forgery (SSRF)
9090
# Block loopback, link-local, multicast, unspecified, and reserved addresses from being pinged.
9191
# reserved addresses include the broadcast address (255.255.255.255)
92-
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:
92+
93+
# 🛡️ Sentinel: Prevent SSRF bypass via IPv4-mapped IPv6 addresses.
94+
# Python's ipaddress module does not apply all IPv4 property checks (like
95+
# is_link_local or is_unspecified) to IPv4-mapped IPv6 addresses (e.g., ::ffff:169.254.169.254).
96+
# We must unwrap the IPv4 address before validating it against the blocklist.
97+
ip_to_check = ip_obj
98+
mapped_ip = getattr(ip_obj, 'ipv4_mapped', None)
99+
if mapped_ip is not None:
100+
ip_to_check = mapped_ip
101+
102+
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:
93103
# 🛡️ Sentinel: Sanitize log input using repr() to prevent CRLF/Log Injection
94104
# IPv6 addresses can contain an arbitrary scope ID (e.g., %eth0\r\n) which is
95105
# not sanitized by ipaddress.ip_address() and could allow log spoofing.

0 commit comments

Comments
 (0)