PT-2026-56836 · Pypi · Pyload-Ng

Published

2026-07-09

·

Updated

2026-07-09

·

CVE-2026-48737

CVSS v3.1

4.9

Medium

VectorAV: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:
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](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 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

MEDIUMCVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L = 4.7
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 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](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 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.

Fix

SSRF

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

CVE-2026-48737
GHSA-M5X5-28JR-GPJJ

Affected Products

Pyload-Ng