CVE-2026-48737: pyload-ng: IPv6 SSRF bypass evades internal-IP guard

GHSA-m5x5-28jr-gpjj MEDIUM
Published July 9, 2026
CISO Take

pyload-ng's SSRF guard relies on Python's `ipaddress.is_global`, which wrongly classifies 6to4 (2002::/16) and NAT64 (64:ff9b::/96) IPv6 addresses as globally routable even when they encode a private or link-local IPv4 underneath, so a URL whose hostname resolves to one of these forms sails past the deny check straight into pycurl's connect logic. This isn't a theoretical edge case for any deployment sitting on IPv6-only cloud subnets with a NAT64 gateway, and the exploit path only needs `Perms.ADD` — any account allowed to queue a download, not an admin. There's no CISA KEV listing, no public exploit or Nuclei template, and EPSS data isn't available; the CVSS AC:H reflects that success depends on the host's network actually routing the transition prefix — but 393 downstream dependents and a working PoC in the advisory make this worth tracking even at MEDIUM severity. No patched version is listed yet in the GHSA, so until one ships, apply the maintainer's suggested unwrap-and-recheck patch to `is_global_address`/`is_global_host`, or block egress to `2002::/16` and `64:ff9b::/96` at the network layer as a stopgap, and audit any deployment where the download queue is exposed to untrusted or semi-trusted users.

Sources: NVD GitHub Advisory ATLAS

What is the risk?

MEDIUM (CVSS 4.7/4.9, AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:N/A:L). Exploitability is gated by network topology: the attacker needs the target host to route 6to4 or NAT64 traffic, which narrows real-world exposure but is realistic for IPv6-only cloud subnets behind a NAT64 gateway or legacy 6to4 tunnels. Privilege bar is low (`Perms.ADD`, held by any user who can queue downloads), and no user interaction is needed once the malicious URL/hostname is submitted. No active exploitation (not in CISA KEV), no EPSS score published, and no public exploit code or Nuclei template exist yet, so this is currently a disclosed-but-unweaponized bug. Package risk score is low (28/100) and this is the third SSRF-class advisory against the same `is_global_address`/`is_global_host` guard, indicating a recurring design weakness rather than a one-off bug.

How does the attack unfold?

Initial Access
Attacker with Perms.ADD (or an AI agent relaying attacker-controlled input) submits a download URL whose hostname has an AAAA record encoding an internal IPv4 via NAT64 or 6to4.
AML.T0049
Defense Evasion
is_global_address/is_global_host misclassify the IPv6 transition address as globally routable because Python's ipaddress.is_global doesn't unwrap the embedded IPv4, so the SSRF deny check passes.
AML.T0107
Exploitation
The pycurl PREREQFUNC allows the connection, and pyload's downloader connects directly to the internal endpoint (e.g., cloud metadata service or RFC1918 host).
Impact
Internal data, credentials, or metadata returned by the internal endpoint are captured in the download output and retrievable by the attacker via pyload's normal interface.
AML.T0025

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Pydantic AI pip <= 0.5.0b3.dev100 No patch
19.4K 479 dependents Pushed 3d ago 92% patched ~189d to patch Full package profile →

Do you use Pydantic AI? You're affected.

How severe is it?

CVSS 3.1
4.9 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Moderate

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC High
PR Low
UI None
S Changed
C Low
I None
A Low

What should I do?

1 step
  1. 1) Apply the maintainer's suggested fix to is_global_address/is_global_host (unwrap 6to4/NAT64-embedded IPv4 via ipv4_mapped/sixtofour/well-known NAT64 prefix checks before evaluating is_global) — track the fix PR referenced in GHSA-m5x5-28jr-gpjj since no version is marked patched yet. 2) As a network-layer compensating control, block outbound routing/forwarding to 2002::/16 (6to4) and 64:ff9b::/96 + 64:ff9b:1::/48 (NAT64) on hosts running pyload, or disable 6to4/NAT64 entirely if not operationally required. 3) Restrict Perms.ADD to trusted accounts only until patched. 4) Monitor curl/pycurl connection logs for outbound attempts to link-local, loopback, or RFC1918 ranges originating from IPv6 transition-prefix hostnames. 5) If pyload is invoked as a tool by an AI agent, add an independent egress allowlist/proxy in front of it rather than trusting the application-layer SSRF guard alone (defense in depth for excessive agency scenarios).

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
NIST AI RMF
MANAGE-4.1 - Third-party risks and vulnerabilities are monitored
OWASP LLM Top 10
LLM06 - Excessive Agency

Frequently Asked Questions

What is CVE-2026-48737?

pyload-ng's SSRF guard relies on Python's `ipaddress.is_global`, which wrongly classifies 6to4 (2002::/16) and NAT64 (64:ff9b::/96) IPv6 addresses as globally routable even when they encode a private or link-local IPv4 underneath, so a URL whose hostname resolves to one of these forms sails past the deny check straight into pycurl's connect logic. This isn't a theoretical edge case for any deployment sitting on IPv6-only cloud subnets with a NAT64 gateway, and the exploit path only needs `Perms.ADD` — any account allowed to queue a download, not an admin. There's no CISA KEV listing, no public exploit or Nuclei template, and EPSS data isn't available; the CVSS AC:H reflects that success depends on the host's network actually routing the transition prefix — but 393 downstream dependents and a working PoC in the advisory make this worth tracking even at MEDIUM severity. No patched version is listed yet in the GHSA, so until one ships, apply the maintainer's suggested unwrap-and-recheck patch to `is_global_address`/`is_global_host`, or block egress to `2002::/16` and `64:ff9b::/96` at the network layer as a stopgap, and audit any deployment where the download queue is exposed to untrusted or semi-trusted users.

Is CVE-2026-48737 actively exploited?

No confirmed active exploitation of CVE-2026-48737 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-48737?

1) Apply the maintainer's suggested fix to `is_global_address`/`is_global_host` (unwrap 6to4/NAT64-embedded IPv4 via `ipv4_mapped`/`sixtofour`/well-known NAT64 prefix checks before evaluating `is_global`) — track the fix PR referenced in GHSA-m5x5-28jr-gpjj since no version is marked patched yet. 2) As a network-layer compensating control, block outbound routing/forwarding to `2002::/16` (6to4) and `64:ff9b::/96` + `64:ff9b:1::/48` (NAT64) on hosts running pyload, or disable 6to4/NAT64 entirely if not operationally required. 3) Restrict `Perms.ADD` to trusted accounts only until patched. 4) Monitor curl/pycurl connection logs for outbound attempts to link-local, loopback, or RFC1918 ranges originating from IPv6 transition-prefix hostnames. 5) If pyload is invoked as a tool by an AI agent, add an independent egress allowlist/proxy in front of it rather than trusting the application-layer SSRF guard alone (defense in depth for excessive agency scenarios).

What systems are affected by CVE-2026-48737?

This vulnerability affects the following AI/ML architecture patterns: agent frameworks, plugin.

What is the CVSS score for CVE-2026-48737?

CVE-2026-48737 has a CVSS v3.1 base score of 4.9 (MEDIUM).

What is the AI security impact?

Affected AI Architectures

agent frameworksplugin

MITRE ATLAS Techniques

AML.T0025 Exfiltration via Cyber Means
AML.T0049 Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Article 15
NIST AI RMF: MANAGE-4.1
OWASP LLM Top 10: LLM06

What are the technical details?

Original Advisory

## Summary `is_global_address` in [`src/pyload/core/utils/web/check.py`](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/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_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](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-cqp8-fcvh-x7r3) / CVE-2026-46678. pyload-ng's prior SSRF advisories [GHSA-7gvf-3w72-p2pg](https://github.com/pyload/pyload/security/advisories/GHSA-7gvf-3w72-p2pg) and [GHSA-8rp3-xc6w-5qp5](https://github.com/pyload/pyload/security/advisories/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`](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/src/pyload/core/api/__init__.py#L582)) requires `Perms.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 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`](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/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`](https://github.com/pyload/pyload/blob/1b12dc7f348db8c144e0f39215680415e90ca4d2/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`](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.

Exploitation Scenario

An authenticated user with `Perms.ADD` (or an AI agent acting on their behalf) submits a download URL whose hostname has an AAAA record set to `64:ff9b::a9fe:a9fe` (NAT64-encoded 169.254.169.254). `is_global_host` resolves the hostname, sees an address inside the NAT64 well-known prefix, and `is_global_address` reports it as globally routable because Python's `ipaddress.is_global` doesn't unwrap the embedded IPv4. The SSRF guard passes, the pycurl `PREREQFUNC` allows the connect, and pyload's downloader establishes a TCP connection to the cloud metadata service. The response — potentially containing IAM credentials or instance metadata — is written back into the downloaded file or job output, which the attacker (or the orchestrating agent, if compromised via a separate vector) can then retrieve through pyload's normal UI/API.

Weaknesses (CWE)

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.

Source: MITRE CWE corpus.

CVSS Vector

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

Timeline

Published
July 9, 2026
Last Modified
July 9, 2026
First Seen
July 9, 2026

Related Vulnerabilities