diff --git a/test_testping1.py b/test_testping1.py index 4c3b4b5..c8ebf97 100644 --- a/test_testping1.py +++ b/test_testping1.py @@ -126,10 +126,34 @@ 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]) + # 🛡️ Sentinel: Test is updated because invalid scope_id is now caught earlier + self.assertIn(r"Invalid IPv6 scope ID: '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_invalid_scope_id(self, mock_call): + """Test is_reachable rejects IPv6 addresses with invalid scope IDs to prevent injection.""" + import ipaddress + + # We must instantiate the IPv6Address manually, as ip_address() strictly parses the string. + # But this tests the case where a pre-instantiated malicious object is passed. + malicious_ips = [ + ipaddress.IPv6Address("fe80::1"), + ipaddress.IPv6Address("fe80::1"), + ipaddress.IPv6Address("fe80::1"), + ] + # Forcibly inject malicious scope IDs into the objects + malicious_ips[0]._scope_id = "eth0;rm -rf /" + malicious_ips[1]._scope_id = "eth0\nls" + malicious_ips[2]._scope_id = "eth0\r\nls" + + for ip in malicious_ips: + with self.assertLogs(level='ERROR') as log: + self.assertFalse(is_reachable(ip)) + self.assertIn("Invalid IPv6 scope ID", 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..cd7e3f4 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,13 @@ def is_reachable(ip, timeout=1): logging.error(f"Invalid IP address format: {repr(ip)}") return False + # 🛡️ Sentinel: Validate IPv6 scope_id to prevent Log and Argument Injection + # The Python ipaddress module allows arbitrary characters (including \n and \r) in the scope_id + 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_obj.scope_id)}") + 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)