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:
+60
-8
@@ -61,6 +61,19 @@ def _hostname_is_blocked(hostname: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# IPv6 ranges that tunnel an IPv4 address at a fixed offset. ``is_global``
|
||||
# judges only the outer address, so an internal IPv4 wrapped in one of these can
|
||||
# pass a check the bare address would fail — 64:ff9b::a9fe:a9fe carries the cloud
|
||||
# metadata address but sits in the 2000::/3 global unicast range.
|
||||
_NAT64_WELL_KNOWN_PREFIX = ipaddress.ip_network('64:ff9b::/96')
|
||||
_IPV4_COMPATIBLE = ipaddress.ip_network('::/96')
|
||||
|
||||
# ``::`` and ``::1`` sit inside ::/96 without being IPv4-compatible addresses
|
||||
# (RFC 4291 reserves both), and 0.0.0.0/8 is not a routable destination anyway.
|
||||
# Reading a tunnelled address out of them would just misdescribe them.
|
||||
_UNUSABLE_IPV4 = ipaddress.ip_network('0.0.0.0/8')
|
||||
|
||||
|
||||
def _normalise_ip(addr: str):
|
||||
"""Parse *addr*, unwrapping IPv4-mapped IPv6 (e.g. ``::ffff:169.254.169.254``)
|
||||
so the embedded IPv4 address is judged on its own merits. Returns ``None``
|
||||
@@ -74,9 +87,46 @@ def _normalise_ip(addr: str):
|
||||
return ip
|
||||
|
||||
|
||||
def _address_is_global(addr: str) -> bool:
|
||||
def _tunnelled_ipv4(ip):
|
||||
"""The IPv4 address an IPv6 transition form tunnels, or ``None``.
|
||||
|
||||
Covers 6to4 (``2002::/16``), Teredo (``2001::/32``), the NAT64 well-known
|
||||
prefix (``64:ff9b::/96``) and the deprecated IPv4-compatible form
|
||||
(``::/96``). IPv4-mapped is handled by ``_normalise_ip`` instead: that form
|
||||
*is* its embedded address rather than a tunnel to it.
|
||||
"""
|
||||
if not isinstance(ip, ipaddress.IPv6Address):
|
||||
return None
|
||||
if ip.sixtofour is not None:
|
||||
return ip.sixtofour
|
||||
if ip.teredo is not None:
|
||||
return ip.teredo[1]
|
||||
if ip in _NAT64_WELL_KNOWN_PREFIX or ip in _IPV4_COMPATIBLE:
|
||||
tunnelled = ipaddress.ip_address(int(ip) & 0xFFFFFFFF)
|
||||
return None if tunnelled in _UNUSABLE_IPV4 else tunnelled
|
||||
return None
|
||||
|
||||
|
||||
def _ips_to_judge(addr: str) -> tuple:
|
||||
"""Every address a verdict on *addr* has to account for: the address itself
|
||||
plus any IPv4 it tunnels. Empty when *addr* is not a valid IP literal.
|
||||
|
||||
A tunnelled address is judged on *both* halves, so unwrapping can only ever
|
||||
tighten the verdict. Returning the embedded address alone would be a way in:
|
||||
Python already rejects all of 2002::/16 and 2001::/32, and replacing
|
||||
``2002:0808:0808::`` with the global 8.8.8.8 would turn an address the guard
|
||||
blocks today into an allowed one.
|
||||
"""
|
||||
ip = _normalise_ip(addr)
|
||||
return ip is not None and ip.is_global
|
||||
if ip is None:
|
||||
return ()
|
||||
tunnelled = _tunnelled_ipv4(ip)
|
||||
return (ip,) if tunnelled is None else (ip, tunnelled)
|
||||
|
||||
|
||||
def _address_is_global(addr: str) -> bool:
|
||||
ips = _ips_to_judge(addr)
|
||||
return bool(ips) and all(ip.is_global for ip in ips)
|
||||
|
||||
|
||||
def _address_allowed_at_connect(addr: str, allow_loopback: bool = False) -> bool:
|
||||
@@ -91,10 +141,12 @@ def _address_allowed_at_connect(addr: str, allow_loopback: bool = False) -> bool
|
||||
169.254.169.254), private (RFC1918), unique-local and every other non-global
|
||||
range.
|
||||
"""
|
||||
ip = _normalise_ip(addr)
|
||||
if ip is None:
|
||||
ips = _ips_to_judge(addr)
|
||||
if not ips:
|
||||
return False
|
||||
return ip.is_global or (allow_loopback and ip.is_loopback)
|
||||
if all(ip.is_global for ip in ips):
|
||||
return True
|
||||
return allow_loopback and all(ip.is_loopback for ip in ips)
|
||||
|
||||
|
||||
def _proxy_endpoint(proxy_url: str):
|
||||
@@ -177,9 +229,9 @@ def install_socket_guard(allow_private: bool = False, proxy_urls=()) -> None:
|
||||
*proxy_urls* are the operator's configured proxies (yt-dlp's ``proxy`` option;
|
||||
the ``*_proxy`` environment variables are picked up automatically). A proxy on
|
||||
loopback is reachable at its own host:port, and nothing else on loopback is.
|
||||
That costs proxied setups nothing: yt-dlp resolves the proxy itself at exactly
|
||||
that host:port, and a media URL is either handed to the proxy unresolved or
|
||||
resolved on its own merits — never inheriting the proxy's allowance.
|
||||
That costs proxied setups nothing: yt-dlp resolves the proxy itself at exactly that host:port,
|
||||
and a media URL is either handed to the proxy unresolved or resolved on its own
|
||||
merits — never inheriting the proxy's allowance.
|
||||
|
||||
When *allow_private* is set (``ALLOW_PRIVATE_ADDRESSES``), the guard is not
|
||||
installed at all, so proxy/VPN setups that route through private or Fake-IP
|
||||
|
||||
Reference in New Issue
Block a user