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 @@ -32,3 +32,7 @@
**Vulnerability:** Log and Argument Injection via unhandled IPv6 `scope_id`
**Learning:** Python's `ipaddress.ip_address` function permits arbitrary characters (like `\n` or `;`) in the `scope_id` section of IPv6 addresses. If the IP address object is logged or used in a subprocess directly without validation, these characters can result in log injection or arbitrary argument execution.
**Prevention:** Strictly validate `scope_id` of IPv6 addresses using a regex like `re.fullmatch(r'[\w\-]+', ip_obj.scope_id)` before they interact with subprocess execution, logging, or other sinks. Wrap unsanitized IP inputs in `repr()` when logging.
## 2025-02-14 - Prevent Unhandled ValueError during repr() and String Conversion
**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>`.
33 changes: 33 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ def test_is_reachable_timeout_too_long(self, mock_call):
self.assertIn("Timeout string too long", log.output[0])
mock_call.assert_not_called()

@patch('testping1.subprocess.call')
def test_is_reachable_massive_int_ip(self, mock_call):
"""Test is_reachable rejects massive integers for IP to prevent DoS."""
with self.assertLogs(level='ERROR') as log:
self.assertFalse(is_reachable(10**10000))
self.assertIn("IP address integer out of range", log.output[0])
mock_call.assert_not_called()

@patch('testping1.subprocess.call')
def test_is_reachable_massive_int_timeout(self, mock_call):
"""Test is_reachable rejects massive integers for timeout to prevent DoS."""
with self.assertLogs(level='ERROR') as log:
self.assertFalse(is_reachable('192.168.1.1', timeout=10**10000))
self.assertIn("Timeout integer out of range", log.output[0])
mock_call.assert_not_called()

@patch('testping1.subprocess.call')
def test_is_reachable_unrepresentable_repr(self, mock_call):
"""Test is_reachable handles ValueError during repr() safely."""
class MaliciousRepr:
def __repr__(self):
raise ValueError("Exceeds limit")

with self.assertLogs(level='ERROR') as log:
self.assertFalse(is_reachable(MaliciousRepr()))
self.assertIn("Invalid IP address format: <unrepresentable>", log.output[0])
mock_call.assert_not_called()

with self.assertLogs(level='ERROR') as log:
self.assertFalse(is_reachable('192.168.1.1', timeout=MaliciousRepr()))
self.assertIn("Invalid timeout value: <unrepresentable>", log.output[0])
mock_call.assert_not_called()

@patch('testping1.subprocess.call')
def test_is_reachable_secure_error_handling(self, mock_call):
"""Test is_reachable handles OSError securely without leaking exceptions."""
Expand Down
44 changes: 33 additions & 11 deletions testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def is_reachable(ip, timeout=1):
if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
ip_obj = ip
else:
# πŸ›‘οΈ Sentinel: Prevent integer string conversion exhaustion (DoS)
# Check integer bounds before passing to ipaddress to avoid ValueError
if type(ip) is int and (ip < 0 or ip > (2**128 - 1)):
logging.error("IP address integer out of range")
return False

# πŸ›‘οΈ Sentinel: Add input length limit to prevent resource exhaustion (DoS)
# The ipaddress module can take significant time to parse extremely long strings
if isinstance(ip, str) and len(ip) > 100:
Expand All @@ -53,7 +59,12 @@ def is_reachable(ip, timeout=1):
ip_obj = ipaddress.ip_address(ip)
except (ValueError, TypeError):
# πŸ›‘οΈ Sentinel: Sanitize log input to prevent CRLF/Log Injection
logging.error(f"Invalid IP address format: {repr(ip)}")
# Handle ValueError from repr() on massive data structures
try:
safe_ip = repr(ip)
except ValueError:
safe_ip = "<unrepresentable>"
logging.error(f"Invalid IP address format: {safe_ip}")
return False

# πŸ›‘οΈ Sentinel: Prevent Log and Argument Injection via IPv6 scope_id
Expand All @@ -62,7 +73,11 @@ def is_reachable(ip, timeout=1):
# injection in the subprocess call or log injection.
if getattr(ip_obj, 'scope_id', None):
if not re.fullmatch(r'[\w\-]+', ip_obj.scope_id):
logging.error(f"Invalid IPv6 scope ID: {repr(ip)}")
try:
safe_ip = repr(ip)
except ValueError:
safe_ip = "<unrepresentable>"
logging.error(f"Invalid IPv6 scope ID: {safe_ip}")
return False

# πŸ›‘οΈ Sentinel: Prevent Server-Side Request Forgery (SSRF)
Expand All @@ -72,18 +87,21 @@ def is_reachable(ip, timeout=1):
# πŸ›‘οΈ 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.
logging.error(f"IP address not allowed for scanning: {repr(ip)}")
try:
safe_ip = repr(ip)
except ValueError:
safe_ip = "<unrepresentable>"
logging.error(f"IP address not allowed for scanning: {safe_ip}")
return False

# πŸ›‘οΈ Sentinel: Validate timeout length to prevent CPU exhaustion (DoS)
# Python's int() conversion for massive strings has O(N^2) complexity.
if isinstance(timeout, str) and len(timeout) > 100:
logging.error("Timeout string too long")
# πŸ›‘οΈ Sentinel: Prevent integer string conversion exhaustion (DoS)
# Reject massive integers before passing them to string formatting/repr()
if type(timeout) is int and (timeout < 0 or timeout > 100):
logging.error("Timeout integer out of range")
return False

# πŸ›‘οΈ Sentinel: Validate timeout to prevent argument injection or errors
# πŸ›‘οΈ Sentinel: Add input length limit to prevent CPU exhaustion (DoS)
# Python's int() conversion algorithm can be exploited with very long strings
# πŸ›‘οΈ Sentinel: Validate timeout length to prevent CPU exhaustion (DoS)
# Python's int() conversion for massive strings has O(N^2) complexity.
if isinstance(timeout, str) and len(timeout) > 100:
logging.error("Timeout string too long")
return False
Expand All @@ -97,7 +115,11 @@ def is_reachable(ip, timeout=1):
# Inputs originating from JSON can include Infinity (parsed as float)
# which raises OverflowError when cast to int and crashes threads.
# πŸ›‘οΈ Sentinel: Sanitize log input to prevent CRLF/Log Injection
logging.error(f"Invalid timeout value: {repr(timeout)}")
try:
safe_timeout = repr(timeout)
except ValueError:
safe_timeout = "<unrepresentable>"
logging.error(f"Invalid timeout value: {safe_timeout}")
return False

# ⚑ Bolt: Optimized ping execution by adding `-n` and `-q` flags.
Expand Down
Loading