Skip to content

GHSA-m5x5-28jr-gpjj CVE-2026-48737 PYSEC-2026-2995

pyLoad: SSRF guard bypass via IPv6 6to4/NAT64 transition wrappers of internal IPs

Medium PyPI
Severity
4.9 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.29%
Chance of exploitation in the next 30 days. Higher than 21.50% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-918 Server-Side Request Forgery (SSRF)
Fix
None yet
No fixed version published

Description

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

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:

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.

Affected versions

PyPI

PackageVulnerableFixed in
pyload-ng
<= 0.5.0b3.dev100
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 3.1 4.9

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
Low

Weakness

CWE-918: Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-68w4-83fh-f2w8
pyload-ng: getUserData/get_userdata exposed at Perms.ANY allow any authenticated account to brute-force the administrator password
High Also affects pyload-ng
GHSA-9q47-3cm2-2rp8
pyLoad: Rate-Limit Bypass and Audit-Log Spoofing via Trusted Client-Controlled `X-Forwarded-For` Header
Medium Also affects pyload-ng
GHSA-jq7h-wrvp-3rgx
pyLoad: Privilege revocation and password change through the REST API do not invalidate the user's session
High Also affects pyload-ng
GHSA-889w-m37p-88m5
pyLoad: Api.set_user_permission never invalidates the target's session
High Also affects pyload-ng
GHSA-p3pr-8f3m-4qp8
pyLoad WindowsPhoneNotify addon: non-admin SETTINGS user triggers SSRF via unguarded http.client notification host
Medium Also affects pyload-ng
GHSA-fr26-jjhm-638c
pyLoad: Tar extraction creates device nodes and FIFOs (member types not filtered; tarfile extractall without filter=)
High Also affects pyload-ng
GHSA-r44w-v6gf-x3p6
pyLoad has an authentication bypass in API key validation (check_apikey cache)
High Also affects pyload-ng
GHSA-j92p-c242-7hfx
pyLoad: Unauthenticated access to /web/<path:filename> bypasses authentication on sensitive templates and leaks internal error details via exception attribute typo
Medium Also affects pyload-ng
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor