Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<unrepresentable>`.
## 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.
10 changes: 10 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
12 changes: 11 additions & 1 deletion testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading