pyload-ng CVE-2026-48737
MEDIUMSeverity by source
AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L
AV:N because URL submission is network-delivered; AC:H because NAT64 or 6to4 network routing is a hard prerequisite; PR:L because Perms.ADD authentication is required; S:C because SSRF crosses trust boundary to internal services.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
Lifecycle Timeline
1DescriptionGitHub Advisory
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":
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 consults the same helper just before TCP-connect:
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.7
AC: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) requiresPerms.ADD, which any account capable of adding links holds. The curl PREREQFUNC athttp_request.py:680is 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
src/pyload/core/utils/web/check.py:
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 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 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:
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.
AnalysisAI
SSRF protection bypass in pyload-ng allows authenticated users with link-add permissions to reach internal network endpoints - including cloud metadata services - by supplying IPv6 transition-encoded addresses that Python's ipaddress.IPv6Address.is_global incorrectly classifies as globally routable. The NAT64 prefix (64:ff9b::/96) bypass is universal across all supported Python versions (3.9-3.14); the 6to4 prefix (2002::/16) bypass additionally affects Python 3.9-3.11, covering the full python_requires = >=3.9 matrix. No confirmed active exploitation (CISA KEV) has been identified, but a researcher-provided proof-of-concept demonstrates full bypass of both the is_global_address guard and the pycurl PREREQFUNC in http_request.py:680.
Technical ContextAI
pyload-ng (pkg:pip/pyload-ng) is a Python download-manager whose outbound SSRF guard is centralised in is_global_address at src/pyload/core/utils/web/check.py. The function delegates to ipaddress.ip_address(value).is_global, a standard library property (CWE-918 root cause: incomplete address-space coverage). Python's IPv6Address.is_global has a documented gap: it considers the NAT64 well-known prefix 64:ff9b::/96 globally routable on every Python 3.9-3.14 release, and the 6to4 prefix 2002::/16 globally routable on Python 3.9-3.11. Both prefixes are IPv6 transition mechanisms that embed an IPv4 address in the low bits - 64:ff9b::a9fe:a9fe wraps 169.254.169.254 (AWS IMDS) and 64:ff9b::7f00:1 wraps 127.0.0.1. Because is_global_address returns True for these forms, the deny-on-non-global check at the pycurl PREREQFUNC (http_request.py:680) is not triggered and the TCP connection to the embedded internal IPv4 proceeds. The same gap propagates through is_global_host, which feeds parse_urls (src/pyload/core/api/__init__.py:582). A prior SSRF class in pyload-ng (GHSA-7gvf-3w72-p2pg, GHSA-8rp3-xc6w-5qp5) addressed redirect-based bypasses through the same helpers; this is an orthogonal IPv6 transition-encoding gap identical in class to pydantic-ai CVE-2026-46678 (GHSA-cqp8-fcvh-x7r3).
RemediationAI
No vendor-released patched version has been confirmed at time of analysis; an upstream fix PR accompanies the advisory but no tagged release incorporating the patch has been independently verified. Operators should monitor https://github.com/pyload/pyload/security/advisories/GHSA-m5x5-28jr-gpjj for a patched release and apply it immediately when available. The suggested fix (included in the advisory) unwraps IPv6 transition-encoding forms via addr.sixtofour (6to4) and explicit range checks against 64:ff9b::/96 and 64:ff9b:1::/48 (NAT64), then re-evaluates the embedded IPv4 address before permitting connections. As compensating controls: block 64:ff9b::/96 and 2002::/16 at the host or perimeter firewall to remove the routing precondition (AC:H requires these prefixes to actually route, so cutting that path eliminates the exploitability condition without changing application behaviour). On cloud instances, enforce IMDSv2 so that SSRF-reached metadata requests require a session-token PUT exchange and plain GET requests to 169.254.169.254 are rejected - this closes the highest-impact exfiltration path. Restrict Perms.ADD in pyload to trusted accounts only to reduce the authenticated attack surface.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Same weakness CWE-918 – Server-Side Request Forgery (SSRF)
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-m5x5-28jr-gpjj