Skip to content
Closed
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 @@ -63,6 +63,10 @@
**Vulnerability:** Server-Side Request Forgery (SSRF) bypass allowing attackers to ping internal IP addresses by wrapping them in IPv6 NAT64 (64:ff9b::/96) or IPv4-compatible (::/96) formats, or by using deprecated site-local addresses (fec0::/10).
**Learning:** Python's `ipaddress` module does not automatically apply `is_private` or SSRF checks to the embedded IPv4 payloads of NAT64 or IPv4-compatible addresses. Additionally, it considers deprecated site-local addresses as globally routable (`is_private` returns False, `is_global` returns True), requiring a specific check using `is_site_local`.
**Prevention:** Always manually unpack and validate the embedded IPv4 payloads for NAT64 and IPv4-compatible IPv6 addresses using bitwise integer operations. For site-local addresses, explicitly check `getattr(ip_obj, 'is_site_local', False)` to ensure backwards-compatibility across Python versions while blocking the deprecated internal range.
## 2024-05-31 - Fix SSRF Bypass via Local-Use IPv4/IPv6 Translation (RFC 8215)
**Vulnerability:** Server-Side Request Forgery (SSRF) bypass allowing attackers to ping internal IP addresses by wrapping them in Local-Use IPv4/IPv6 Translation formats (RFC 8215, 64:ff9b:1::/96).
**Learning:** Python's `ipaddress` module does not natively unwrap these embedded IPv4 addresses. Similar to NAT64, it evaluates these addresses as `is_global = False` but `is_private = True`, which can lead to bypass if only `is_global` check is present and unwrapping logic is missing.
**Prevention:** To prevent SSRF bypasses via Local-Use IPv4/IPv6 Translation addresses, manually unwrap the embedded IPv4 address by checking if the high 96 bits match the prefix (`ip_int >> 32 == 0x0064ff9b0001000000000000`) and extract the underlying 32-bit IPv4 address using bitwise operations (`ip_int & 0xFFFFFFFF`) to validate it against SSRF rules.
## 2025-02-28 - SSRF Bypass via Carrier-Grade NAT (CGNAT)
**Vulnerability:** The Python `ipaddress` module's `is_private` property evaluates Carrier-Grade NAT (CGNAT) addresses (e.g., `100.64.0.0/10`) and benchmark ranges as `False`. This allows attackers to bypass standard SSRF filters and potentially scan internal shared infrastructure if the hosting environment natively routes those non-public ranges.
**Learning:** `is_private` is not a catch-all for internal or non-routable IPs. The module maintains a separate `is_global` property that more accurately defines publicly routable internet addresses.
Expand Down
8 changes: 6 additions & 2 deletions test_testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,12 @@ def test_is_reachable_ssrf_bypass_teredo(self, mock_call):

@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."""
ssrf_ips = ['64:ff9b::127.0.0.1', '64:ff9b::192.168.1.1', '::127.0.0.1', '::192.168.1.1']
"""Test is_reachable prevents SSRF bypass via NAT64, Local-Use Translation (RFC 8215) and IPv4-compatible addresses."""
ssrf_ips = [
'64:ff9b::127.0.0.1', '64:ff9b::192.168.1.1', # NAT64
'64:ff9b:1::127.0.0.1', '64:ff9b:1::192.168.1.1', # Local-Use Translation (RFC 8215)
'::127.0.0.1', '::192.168.1.1' # IPv4-compatible
]
for ip in ssrf_ips:
with self.assertLogs(level='ERROR') as log:
self.assertFalse(is_reachable(ip))
Expand Down
2 changes: 2 additions & 0 deletions testping1.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,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 == 0x0064ff9b0001000000000000: # Local-Use IPv4/IPv6 Translation 64:ff9b:1::/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
Expand Down
Loading