Skip to content
Open
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 @@ -75,3 +75,7 @@
**Vulnerability:** Python's `ipaddress.ip_address()` function accepts both `str` and `bytes`. When passing an extremely large bytes object (e.g., `b"A" * 10**8`), the module can take several seconds to raise a `ValueError` due to inefficient internal parsing logic, leading to a CPU exhaustion Denial of Service (DoS).
**Learning:** Checking the length of `str` inputs before passing them to `ipaddress.ip_address()` is not sufficient to prevent DoS, as an attacker could pass a massive `bytes` object if the function accepts polymorphic types.
**Prevention:** Always enforce strict length limits (e.g., <= 100 characters/bytes) on *both* `str` and `bytes` inputs before attempting to parse them using the `ipaddress` module. Use `isinstance(ip, (str, bytes))` and check `len()`.
## 2025-05-12 - Prevent SSRF Bypass via ISATAP Tunneling Addresses
**Vulnerability:** The Python `ipaddress` module evaluates ISATAP addresses (e.g., `2001:db8::5efe:127.0.0.1`) as `is_global = True` and lacks a native `is_isatap` property. This can allow attackers to bypass SSRF protections by encapsulating restricted IPv4 addresses (like `127.0.0.1` or `192.168.1.1`) inside an ISATAP IPv6 address.
**Learning:** Python's standard `ipaddress` module does not intrinsically unwrap or identify ISATAP encapsulated payloads. We must manually inspect the 32-bit ISATAP identifier and unwrap it.
**Prevention:** Extract the 32-bit ISATAP identifier using `(ip_int >> 32) & 0xFFFFFFFF`. If it matches `0x00005efe` or `0x02005efe`, extract the underlying IPv4 address using `ip_int & 0xFFFFFFFF` and validate it against the SSRF rules.
10 changes: 10 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@ def test_is_reachable_ssrf_bypass_teredo(self, mock_call):
self.assertIn("IP address not allowed for scanning", log.output[0])
mock_call.assert_not_called()

@patch('testping1.subprocess.call')
def test_is_reachable_ssrf_bypass_isatap(self, mock_call):
"""Test is_reachable prevents SSRF bypass via ISATAP tunneling addresses."""
ssrf_ips = ['2000::5efe:127.0.0.1', '2000::0200:5efe:127.0.0.1']
for ip in ssrf_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_ssrf_bypass_nat64_and_compat(self, mock_call):
"""Test is_reachable prevents SSRF bypass via NAT64 and IPv4-compatible addresses."""
Expand Down
5 changes: 5 additions & 0 deletions testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ def is_reachable(ip, timeout=1):
unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF)
elif ip_int < 2**32 and ip_int not in (0, 1): # IPv4-compatible ::w.x.y.z
unwrapped = ipaddress.IPv4Address(ip_int)
else:
# πŸ›‘οΈ Sentinel: Prevent SSRF bypass via ISATAP tunneling addresses
isatap_id = (ip_int >> 32) & 0xFFFFFFFF
if isatap_id in (0x00005efe, 0x02005efe):
unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF)

if unwrapped is not None:
is_blocked = not unwrapped.is_global or unwrapped.is_multicast
Expand Down
Loading