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:
nesquena-hermes committed 2026-07-02 18:04:28 +00:00
1 parent 15dc671c0a
commit bf1ca227dd
2 files changed
+39 -2

No files matched your search

+12 -2
View File
@@ -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
+27
View File
@@ -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 [