fix(tts): block non-globally-routable addrs (CGNAT/Tailscale 100.64.0.0/10) via not-is_global backstop + tests (Opus follow-up on #5407)
This commit is contained in:
1 parent
15dc671c0a
commit
bf1ca227dd
2 files changed
+39
-2
No files matched your search
+12
-2
@@ -16302,7 +16302,16 @@ _TTS_LOCALHOST_HOSTS = {"127.0.0.1", "::1", "localhost"}
|
||||
|
||||
|
||||
def _tts_addr_is_blocked(ip_str: str) -> bool:
|
||||
"""Return True when IP is in a private or otherwise non-routable class."""
|
||||
"""Return True when IP is in a private or otherwise non-routable class.
|
||||
|
||||
The explicit flags below document the concrete SSRF-risk classes, but the
|
||||
load-bearing rule is the ``not is_global`` backstop: it blocks every address
|
||||
that is not globally routable — including ranges the named flags miss, most
|
||||
notably ``100.64.0.0/10`` (RFC 6598 CGNAT, also Tailscale's default address
|
||||
space), the ``198.18.0.0/15`` benchmarking range, and any future
|
||||
non-global allocation — so a rebinding host cannot reach a victim's tailnet
|
||||
or carrier-NAT peer.
|
||||
"""
|
||||
import ipaddress
|
||||
|
||||
try:
|
||||
@@ -16310,7 +16319,8 @@ def _tts_addr_is_blocked(ip_str: str) -> bool:
|
||||
except ValueError:
|
||||
return False
|
||||
return (
|
||||
ip.is_private
|
||||
not ip.is_global
|
||||
or ip.is_private
|
||||
or ip.is_loopback
|
||||
or ip.is_link_local
|
||||
or ip.is_reserved
|
||||
|
||||
@@ -201,6 +201,33 @@ def test_tts_resolve_pinned_address_rejects_blocked_target(monkeypatch):
|
||||
routes._tts_resolve_pinned_address("public.example.com")
|
||||
|
||||
|
||||
def test_tts_addr_is_blocked_covers_non_global_ranges():
|
||||
# The `not is_global` backstop must block ranges the named private/loopback/
|
||||
# link-local flags miss — most importantly RFC 6598 CGNAT (100.64.0.0/10,
|
||||
# also Tailscale's default space) so a rebinding host can't reach a victim's
|
||||
# tailnet/carrier-NAT peer — while genuine public addresses stay allowed.
|
||||
assert routes._tts_addr_is_blocked("100.64.0.1") is True # CGNAT / Tailscale
|
||||
assert routes._tts_addr_is_blocked("100.127.255.254") is True # CGNAT upper edge
|
||||
assert routes._tts_addr_is_blocked("198.18.0.1") is True # benchmarking (RFC 2544)
|
||||
assert routes._tts_addr_is_blocked("192.0.2.5") is True # TEST-NET-1 (docs, non-global)
|
||||
# Real public addresses still pass through:
|
||||
assert routes._tts_addr_is_blocked("1.1.1.1") is False
|
||||
assert routes._tts_addr_is_blocked("8.8.8.8") is False
|
||||
assert routes._tts_addr_is_blocked("140.82.112.3") is False # github.com range
|
||||
# 203.0.113.0/24 is TEST-NET-3 (documentation, non-global) -> blocked too:
|
||||
assert routes._tts_addr_is_blocked("203.0.113.10") is True
|
||||
|
||||
|
||||
def test_tts_resolve_pinned_address_rejects_cgnat_rebind_target(monkeypatch):
|
||||
# A host that resolves into the CGNAT/Tailscale range must be rejected at
|
||||
# pinning time (regression for the 100.64.0.0/10 blocklist gap).
|
||||
def _fake_getaddrinfo(*_args, **_kwargs):
|
||||
return [(0, 0, 0, "", ("100.64.12.34", 0))]
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _fake_getaddrinfo)
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
routes._tts_resolve_pinned_address("tailnet-rebind.example.com")
|
||||
|
||||
|
||||
def test_tts_resolve_pinned_address_rejects_mixed_addresses(monkeypatch):
|
||||
def _fake_getaddrinfo(*_args, **_kwargs):
|
||||
return [
|
||||
|
||||
Reference in new issue
Block a user