PT-2026-56836 · Pypi · Pyload-Ng
Published
2026-07-09
·
Updated
2026-07-09
·
CVE-2026-48737
CVSS v3.1
4.9
Medium
| Vector | AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L |
Summary
is global address in src/pyload/core/utils/web/check.py is the central guard against SSRF-style outbound connections in pyload-ng. It tests whether a given IP is "globally routable" via Python's ipaddress.ip address(value).is global, and callers treat not is global as "deny":python
def is global address(value):
try:
return ipaddress.ip address(value).is global
except ValueError:
return False
def is global host(value):
ips = host to ip(value)
return ips and all((is global address(ip) for ip in ips))Python's
ipaddress.IPv6Address.is global classifies the NAT64 well-known prefix as globally routable on every supported Python version (3.9 through 3.14 confirmed), and on older Pythons (3.9-3.11) the 6to4 prefix as well:| address | is global on Py 3.9-3.11 | is global on Py 3.12+ | wrapped IPv4 |
|---|---|---|---|
2002:7f00:0001:: (6to4) | True | False | 127.0.0.1 |
2002:0a00:0001:: (6to4) | True | False | 10.0.0.1 |
2002:a9fe:a9fe:: (6to4) | True | False | 169.254.169.254 (IMDS) |
64:ff9b::a9fe:a9fe (NAT64) | True | True | 169.254.169.254 |
64:ff9b::7f00:1 (NAT64) | True | True | 127.0.0.1 |
pyload-ng declares
python requires = >=3.9 (setup.cfg), so deployments on Python 3.9-3.11 see the 6to4 path too. The NAT64 path is universal. is global returns True for these wrappers, so is global address returns True and the deny check passes. The pycurl PREREQFUNC at [src/pyload/core/network/http/http request.py:680](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/src/pyload/core/network/http/http request.py#L680) consults the same helper just before TCP-connect:python
if not self.allow private ip:
is proxy ip = self.http proxy host and self.http proxy host == (conn primary ip, conn primary port)
if not is global address(conn primary ip) and not is proxy ip:
return pycurl.PREREQFUNC ABORT
return pycurl.PREREQFUNC OKOn a host with 6to4 routing (legacy operator tunnels;
2002::/16 still configurable) or NAT64 (cloud IPv6-only subnets with NAT64 gateway), the encoded form routes to the embedded IPv4 and the curl connection terminates at the internal endpoint, defeating the deny.is global host (the helper that callers like parse urls use against a URL hostname) feeds through host to ip which pins family=AF INET, so hostname-based reach to these forms relies on the attacker supplying an IPv6 literal in the URL — but the curl PREREQFUNC sees the actual resolved IP (the AAAA returned for the hostname), so a hostname with an AAAA record set to one of the bypass forms reaches the same gap.Cross-reference: this is the same incomplete-coverage class as pydantic-ai's GHSA-cqp8-fcvh-x7r3 / CVE-2026-46678. pyload-ng's prior SSRF advisories GHSA-7gvf-3w72-p2pg and GHSA-8rp3-xc6w-5qp5 both went through
is global host / is global address; the IPv6 transition gap is orthogonal to those redirect-bypass classes.Severity
MEDIUM —
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L = 4.7AC:H— exploitation requires the host network to route 6to4 (2002::/16traffic), have a NAT64 gateway, or otherwise resolve the IPv6 transition form to an internal IPv4 endpoint at the TCP layer.PR:L—parse urls([src/pyload/core/api/ init .py:582](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/src/pyload/core/api/ init .py#L582)) requiresPerms.ADD, which any account capable of adding links holds. The curl PREREQFUNC at [http request.py:680](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/http request.py#L680) is reached by every downloader plugin that runs afteris global hostpassed.C:L/A:L— internal-network recon and timing-based confirmation; cloud-metadata exfiltration on networks where the transition form actually routes.
CWE-918: Server-Side Request Forgery (SSRF).
Affected versions
pyload-ng from the introduction of is global address / is global host in src/pyload/core/utils/web/check.py up to and including the current main HEAD as of filing.Vulnerable code
python
def is global address(value):
try:
return ipaddress.ip address(value).is global
except ValueError:
return FalsePython ipaddress.IPv6Address.is global returns True for every address in 2002::/16 (6to4) and 64:ff9b::/96 (NAT64) regardless of the IPv4 they wrap, so this guard is a one-line bypass for the prefix the attacker chooses.Reproduction
[
research wave5/poc/pyload ipv6 ssrf/poc.py](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/research wave5/poc/pyload ipv6 ssrf/poc.py) drives both is global address and is global host against IPv6 transition forms whose embedded IPv4 points at loopback, RFC 1918, and AWS IMDS. The helper returns "globally routable" for every form. A second pass replays the same forms through the PREREQFUNC logic in [http request.py:680](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/http request.py#L680) and shows the connection would be ALLOWED in each case.Suggested fix
Treat IPv6 transition-encoding forms by unwrapping the embedded IPv4 and re-running the global check, plus an explicit blocklist of well-known embedding prefixes for defence in depth:
python
import ipaddress
NAT64 WELL KNOWN = ipaddress.IPv6Network("64:ff9b::/96")
NAT64 DISCOVERY = ipaddress.IPv6Network("64:ff9b:1::/48")
def embedded ipv4(addr):
if isinstance(addr, ipaddress.IPv6Address):
if addr.ipv4 mapped is not None:
return addr.ipv4 mapped
if addr.sixtofour is not None: # 2002::/16 6to4
return addr.sixtofour
if addr in NAT64 WELL KNOWN or addr in NAT64 DISCOVERY:
return ipaddress.IPv4Address(addr.packed[-4:])
return None
def is global address(value):
try:
addr = ipaddress.ip address(value)
except ValueError:
return False
embedded = embedded ipv4(addr)
if embedded is not None:
addr = embedded
return addr.is globalA patch implementing this approach (plus tests covering 6to4 and NAT64 wraps for 127.0.0.1, 10.0.0.1, 172.16.0.1, 192.168.1.1, 100.64.0.0/10, and 169.254.169.254) accompanies the fix PR.
Credits
Reported by tonghuaroot.
Fix
SSRF
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Pyload-Ng