mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 13:35:01 +00:00
fix: judge the IPv4 tunnelled inside IPv6 transition addresses
The SSRF guard classified addresses with ipaddress.is_global, which looks only at the outer address. An IPv6 form that carries an IPv4 address at a fixed offset therefore passed a check the bare address would have failed: the NAT64 well-known prefix 64:ff9b::/96 sits in the 2000::/3 global unicast range, so 64:ff9b::a9fe:a9fe was accepted while 169.254.169.254 was refused. The deprecated IPv4-compatible form ::/96 has the same property. Both the ingress validator and the connect-time socket guard classified through the same helper, so both were affected. Judge every address a verdict has to account for: the outer address plus any IPv4 it tunnels, allowed only when all of them are global. Unwrapping this way can only tighten the verdict, which matters for 6to4 and Teredo — Python already rejects 2002::/16 and 2001::/32 wholesale, and replacing the outer address with its payload would have turned 2002:0808:0808:: from blocked into allowed. Reaching an internal service this way additionally requires NAT64 routing on the host network, which the attacker does not control. Reported by tonghuaroot in GHSA-5mq5-qr7m-f4wx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import url_guard
|
||||
from url_guard import (
|
||||
validate_url,
|
||||
_address_allowed_at_connect,
|
||||
_address_is_global,
|
||||
_guarded_getaddrinfo,
|
||||
_proxy_endpoint,
|
||||
install_socket_guard,
|
||||
@@ -138,6 +139,47 @@ class ConnectAddressPolicyTests(unittest.TestCase):
|
||||
self.assertFalse(_address_allowed_at_connect("::ffff:169.254.169.254"))
|
||||
|
||||
|
||||
class TunnelledIPv4Tests(unittest.TestCase):
|
||||
"""IPv6 transition forms that carry an IPv4 address the outer address hides.
|
||||
|
||||
``is_global`` looks only at the outer address, so a form that tunnels an
|
||||
internal IPv4 has to be unwrapped before it is judged (GHSA-5mq5-qr7m-f4wx).
|
||||
"""
|
||||
|
||||
def test_nat64_well_known_prefix_blocked(self):
|
||||
# 2000::/3 global unicast on its face; carries the metadata address.
|
||||
self.assertFalse(_address_is_global("64:ff9b::a9fe:a9fe"))
|
||||
self.assertFalse(_address_is_global("64:ff9b::7f00:1"))
|
||||
self.assertFalse(_address_allowed_at_connect("64:ff9b::a9fe:a9fe"))
|
||||
|
||||
def test_nat64_carrying_a_public_address_allowed(self):
|
||||
self.assertTrue(_address_is_global("64:ff9b::8.8.8.8"))
|
||||
|
||||
def test_ipv4_compatible_form_blocked(self):
|
||||
# The deprecated ::/96 form, likewise global-looking to is_global.
|
||||
self.assertFalse(_address_is_global("::a9fe:a9fe"))
|
||||
self.assertFalse(_address_allowed_at_connect("::a9fe:a9fe"))
|
||||
|
||||
def test_sixtofour_and_teredo_stay_blocked(self):
|
||||
# Python rejects these ranges wholesale. Unwrapping must not promote a
|
||||
# blocked address to an allowed one just because the payload is global.
|
||||
self.assertFalse(_address_is_global("2002:a9fe:a9fe::"))
|
||||
self.assertFalse(_address_is_global("2002:0808:0808::"))
|
||||
self.assertFalse(_address_is_global("2001:0:4136:e378:8000:63bf:3fff:fdd2"))
|
||||
|
||||
def test_plain_addresses_unaffected(self):
|
||||
self.assertTrue(_address_is_global("142.250.1.1"))
|
||||
self.assertTrue(_address_is_global("2607:f8b0:4004:c07::64"))
|
||||
self.assertFalse(_address_is_global("not-an-ip"))
|
||||
|
||||
def test_tunnelled_form_blocked_at_ingress(self):
|
||||
with mock.patch(
|
||||
"url_guard.socket.getaddrinfo",
|
||||
return_value=_addrinfo("64:ff9b::a9fe:a9fe", family=socket.AF_INET6),
|
||||
):
|
||||
self.assertIsNotNone(validate_url("http://nat64.example/x"))
|
||||
|
||||
|
||||
class ProxyEndpointParsingTests(unittest.TestCase):
|
||||
def test_explicit_port(self):
|
||||
self.assertEqual(_proxy_endpoint("http://127.0.0.1:9050"), ("127.0.0.1", 9050))
|
||||
|
||||
Reference in New Issue
Block a user