Skip to main content

pyload-ng CVE-2026-48737

MEDIUM
Server-Side Request Forgery (SSRF) (CWE-918)
2026-07-09 https://github.com/pyload/pyload GHSA-m5x5-28jr-gpjj
4.9
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
4.9 MEDIUM
AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L
vuln.today AI
4.9 MEDIUM

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.

3.1 AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L
4.0 AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:L/SC:L/SI:N/SA:N

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
Low

Lifecycle Timeline

1
Analysis Generated
Jul 09, 2026 - 14:21 vuln.today

DescriptionGitHub 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":

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:

addressis_global on Py 3.9-3.11is_global on Py 3.12+wrapped IPv4
2002:7f00:0001:: (6to4)TrueFalse127.0.0.1
2002:0a00:0001:: (6to4)TrueFalse10.0.0.1
2002:a9fe:a9fe:: (6to4)TrueFalse169.254.169.254 (IMDS)
64:ff9b::a9fe:a9fe (NAT64)TrueTrue169.254.169.254
64:ff9b::7f00:1 (NAT64)TrueTrue127.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:

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_OK

On 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::/16 traffic), 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) requires Perms.ADD, which any account capable of adding links holds. The curl PREREQFUNC at http_request.py:680 is reached by every downloader plugin that runs after is_global_host passed.
  • 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:

python
def is_global_address(value):
    try:
        return ipaddress.ip_address(value).is_global
    except ValueError:
        return False

Python 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:

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_global

A 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.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

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

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-49869 CRITICAL POC
10.0 Jun 26

Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

Share

CVE-2026-48737 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy