diff --git a/.jules/sentinel.md b/.jules/sentinel.md index bdca677..fa4d44c 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -27,3 +27,8 @@ **Vulnerability:** Application crashes and potential DoS due to `OverflowError` when untrusted input containing `Infinity` or `NaN` is parsed (e.g., from JSON) and subsequently converted to an integer using `int()`. **Learning:** Python's `int()` function raises `OverflowError` instead of `ValueError` or `TypeError` when it encounters infinite float values. If not explicitly caught, this can crash worker thread pools processing untrusted user input. **Prevention:** When converting untrusted input to integers using `int()`, explicitly catch `OverflowError` alongside `ValueError` and `TypeError` to ensure graceful handling. + +## 2024-05-18 - IPv6 scope_id Argument and Log Injection Prevention +**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. diff --git a/test_testping1.py b/test_testping1.py index 4c3b4b5..39ada39 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -126,10 +126,31 @@ def test_is_reachable_ssrf_log_injection(self, mock_call): with self.assertLogs(level='ERROR') as log: self.assertFalse(is_reachable(malicious_ip)) - self.assertIn(r"IP address not allowed for scanning: 'fe80::1%eth0\nERROR:root:System Compromised'", log.output[0]) + self.assertIn(r"Invalid IPv6 scope ID: 'fe80::1%eth0\nERROR:root:System Compromised'", log.output[0]) self.assertNotIn("\nERROR:root:System Compromised", log.output[0]) mock_call.assert_not_called() + @patch('testping1.subprocess.call') + def test_is_reachable_ipv6_scope_id_validation(self, mock_call): + """Test is_reachable rejects arbitrary and potentially malicious IPv6 scope_ids.""" + import ipaddress + # Manually instantiate and inject malicious scope_id to bypass parsing + malicious_ip_obj = ipaddress.IPv6Address('fe80::1') + malicious_ip_obj._scope_id = 'eth0; ls' + + with self.assertLogs(level='ERROR') as log: + self.assertFalse(is_reachable(malicious_ip_obj)) + self.assertIn("Invalid IPv6 scope ID: IPv6Address('fe80::1%eth0; ls')", log.output[0]) + mock_call.assert_not_called() + + malicious_ip_obj2 = ipaddress.IPv6Address('fe80::1') + malicious_ip_obj2._scope_id = 'eth0\nERROR:root:Compromised' + + with self.assertLogs(level='ERROR') as log: + self.assertFalse(is_reachable(malicious_ip_obj2)) + self.assertIn("Invalid IPv6 scope ID: IPv6Address('fe80::1%eth0\\nERROR:root:Compromised')", log.output[0]) + mock_call.assert_not_called() + @patch('testping1.subprocess.call') def test_is_reachable_subprocess_timeout(self, mock_call): """Test is_reachable handles subprocess.TimeoutExpired securely.""" diff --git a/testping1.py b/testping1.py index 8f9dad3..35690ab 100644 --- a/testping1.py +++ b/testping1.py @@ -4,6 +4,7 @@ import ipaddress import logging import shutil +import re from tqdm import tqdm # Install with `pip install tqdm` # ⚡ Bolt: Cached DEVNULL file descriptor to minimize subprocess spawn overhead @@ -53,6 +54,15 @@ def is_reachable(ip, timeout=1): logging.error(f"Invalid IP address format: {repr(ip)}") return False + # 🛡️ Sentinel: Prevent Log and Argument Injection via IPv6 scope_id + # The python ipaddress module allows arbitrary characters (including \n and ;) in + # the scope_id of IPv6 addresses. If unhandled, this can lead to argument + # 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)}") + return False + # 🛡️ 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)