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
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-30 - SSRF Bypass via ISATAP (RFC 5214) Tunneling Addresses
**Vulnerability:** Python's `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 allows attackers to bypass SSRF validations by embedding private or reserved IPv4 addresses inside ISATAP IPv6 structures.
**Learning:** `ipaddress` module does not intrinsically unwrap and validate all forms of IPv6 encapsulation.
**Prevention:** To prevent SSRF bypasses via ISATAP 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.
12 changes: 12 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ def test_is_reachable_ssrf_bypass_ipv4_mapped(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."""
# 2001:db8::5efe:127.0.0.1 encapsulates 127.0.0.1
# 2001:db8::200:5efe:192.168.1.1 encapsulates 192.168.1.1
ssrf_ips = ['2001:db8::5efe:127.0.0.1', '2001:db8::200:5efe:192.168.1.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_cgnat_prevention(self, mock_call):
"""Test is_reachable prevents SSRF by rejecting Carrier-Grade NAT (CGNAT) IPs."""
Expand Down
6 changes: 5 additions & 1 deletion 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
# πŸ›‘οΈ 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_id = (ip_int >> 32) & 0xFFFFFFFF
if isatap_id in (0x00005efe, 0x02005efe): # ISATAP tunnel
unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF)

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