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 @@ -80,3 +80,8 @@
**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.

## 2025-05-18 - SSRF Bypass via SIIT (IPv4-translated) Addresses
**Vulnerability:** Attackers could bypass SSRF IP blocklists using SIIT (Stateless IP/ICMP Translation, RFC 2765) addresses. The format `::ffff:0:a.b.c.d` (using the `::ffff:0:0:0/96` prefix) evaluates as `is_global = True` in Python's `ipaddress` module and is NOT caught by the `ipv4_mapped` property. If an attacker passes such an address, the OS networking stack might route it directly to the embedded IPv4 target, bypassing internal security restrictions.
**Learning:** Python's `ipaddress` module only natively extracts standard IPv4-mapped addresses (`::ffff:a.b.c.d`), failing to recognize or unwrap SIIT IPv4-translated addresses.
**Prevention:** Always manually unwrap SIIT addresses by checking if the high 96 bits of the IPv6 integer match the SIIT prefix (`ip_int >> 32 == 0xffff0000`). If so, extract the underlying 32-bit IPv4 address using bitwise operations (`ip_int & 0xFFFFFFFF`) and validate it against the SSRF blocklist.
11 changes: 11 additions & 0 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ 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_siit(self, mock_call):
"""Test is_reachable prevents SSRF bypass via SIIT (IPv4-translated) addresses."""
# ::ffff:0:a.b.c.d encapsulates an IPv4 address
ssrf_ips = ['::ffff:0:127.0.0.1', '::ffff:0: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_bypass_isatap(self, mock_call):
"""Test is_reachable prevents SSRF bypass via ISATAP tunneling addresses."""
Expand Down
2 changes: 2 additions & 0 deletions testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ def is_reachable(ip, timeout=1):
unwrapped = None
if ip_int >> 32 == 0x0064ff9b0000000000000000: # NAT64 64:ff9b::/96
unwrapped = ipaddress.IPv4Address(ip_int & 0xFFFFFFFF)
elif ip_int >> 32 == 0xffff0000: # SIIT (IPv4-translated) ::ffff:0:a.b.c.d
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:
Expand Down
Loading