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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@
**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()`.

## 2024-05-11 - SSRF Bypass via ISATAP IPv6 Addresses
**Vulnerability:** The application was vulnerable to an SSRF bypass when passed ISATAP tunneling addresses (e.g., `2001:db8::5efe:127.0.0.1` or `2001:db8::200:5efe:127.0.0.1`), which encapsulate a blocked internal IPv4 address.
**Learning:** Python's `ipaddress` module evaluates these addresses as `is_global = True` and does not provide an `isatap` property to easily extract the embedded IPv4 address.
**Prevention:** To prevent SSRF bypasses via ISATAP (RFC 5214) tunneling addresses, manually identify and unwrap the embedded IPv4 address. Extract the 32-bit ISATAP identifier using `(ip_int >> 32) & 0xFFFFFFFF` and check for `0x00005efe` or `0x02005efe`, then validate the underlying IPv4 address against SSRF rules.
14 changes: 14 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ 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 addresses."""
ssrf_ips = [
'2001:db8::5efe:127.0.0.1',
'fe80::5efe:192.168.1.1',
'2001:db8::200: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
8 changes: 6 additions & 2 deletions testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,18 @@ def is_reachable(ip, timeout=1):
not t_cli.is_global or t_cli.is_multicast
)
else:
# πŸ›‘οΈ Sentinel: Unpack NAT64 (RFC 6052) and IPv4-compatible (RFC 4291) addresses manually
# as Python's ipaddress module does not natively unwrap them for SSRF checks.
# πŸ›‘οΈ Sentinel: Unpack NAT64 (RFC 6052), IPv4-compatible (RFC 4291), and ISATAP (RFC 5214)
# addresses manually as Python's ipaddress module does not natively unwrap them for SSRF checks.
ip_int = int(ip_obj)
unwrapped = None
if ip_int >> 32 == 0x0064ff9b0000000000000000: # NAT64 64:ff9b::/96
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:
isatap_identifier = (ip_int >> 32) & 0xFFFFFFFF
if isatap_identifier == 0x00005efe or isatap_identifier == 0x02005efe: # ISATAP
unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF)

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