VYPR
Critical severity9.1NVD Advisory· Published Apr 6, 2026· Updated Apr 20, 2026

CVE-2026-35459

CVE-2026-35459

Description

pyLoad is a free and open-source download manager written in Python. In 0.5.0b3.dev96 and earlier, pyLoad has a server-side request forgery (SSRF) vulnerability. The fix for CVE-2026-33992 added IP validation to BaseDownloader.download() that checks the hostname of the initial download URL. However, pycurl is configured with FOLLOWLOCATION=1 and MAXREDIRS=10, causing it to automatically follow HTTP redirects. Redirect targets are never validated against the SSRF filter. An authenticated user with ADD permission can bypass the SSRF fix by submitting a URL that redirects to an internal address.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pyload-ngPyPI
<= 0.5.0b3.dev96

Affected products

1

Patches

1
33c55da08432

fix GHSA-7gvf-3w72-p2pg security advisory

https://github.com/pyload/pyloadGammaC0deApr 2, 2026via ghsa
5 files changed · +26 13
  • src/pyload/core/network/http/http_chunk.py+2 0 modified
    @@ -133,6 +133,7 @@ def __init__(self, id, parent, range=None, resume=False):
     
             self.code = 0  #: last http code, set by parent
     
    +        self.allow_private_ip = False
             self.aborted = False  # indicates that the chunk aborted gracefully
     
             self.c = pycurl.Curl()
    @@ -189,6 +190,7 @@ def get_handle(self):
             )
             self.c.setopt(pycurl.WRITEFUNCTION, self._write_body_callback)
             self.c.setopt(pycurl.HEADERFUNCTION, self._write_header_callback)
    +        self.c.setopt(pycurl.PREREQFUNCTION, self._pre_request_callback)
     
             # request all bytes, since some servers in russia seems to have a defect
             # arithmetic unit
    
  • src/pyload/core/network/http/http_download.py+9 2 modified
    @@ -1,11 +1,12 @@
     import os
     import time
    +import urllib
     from logging import getLogger
     
     import pycurl
     from pyload import APPID
     
    -from ..exceptions import Abort
    +from ..exceptions import Abort, Fail
     from .http_chunk import ChunkInfo, HTTPChunk
     from .http_request import BadHeader
     
    @@ -142,8 +143,13 @@ def download(self, chunks=1, resume=False):
                         self.close_chunk(chunk)
     
                     return self._download(chunks, False)
    +            elif code == 42:
    +                hostname = urllib.parse.urlparse(self.url).hostname
    +                raise Fail(f"Refusing to download from Server-Side host '{hostname}'")
    +
                 else:
                     raise
    +
             finally:
                 self.close()
     
    @@ -239,7 +245,8 @@ def _download(self, chunks, resume):
                         if errno != pycurl.E_WRITE_ERROR or not chunk.aborted:
                             failed.append(chunk)
                             ex = pycurl.error(errno, msg)
    -                        self.log.debug(f"Chunk {chunk.id + 1} failed: {ex}")
    +                        if errno != pycurl.E_ABORTED_BY_CALLBACK:
    +                            self.log.debug(f"Chunk {chunk.id + 1} failed: {ex}")
                             continue
     
                         try:  #: check if the header implies success, else add it to failed list
    
  • src/pyload/core/network/http/http_request.py+13 0 modified
    @@ -16,6 +16,7 @@
     
     from ...utils.check import is_mapping
     from ...utils.convert import to_bytes, to_str
    +from ...utils.web.check import is_global_address
     from ...utils.web.parse import http_header as parse_header_line
     from ...utils.web.purge import unescape as html_unescape
     from ..exceptions import Abort
    @@ -71,6 +72,7 @@ class HTTPRequest:
         def __init__(self, cookies=None, options=None, limit=2_000_000):
             self.exception = None
             self.limit = limit
    +        self.allow_private_ip = True
     
             self.c = pycurl.Curl()
     
    @@ -98,6 +100,7 @@ def __init__(self, cookies=None, options=None, limit=2_000_000):
     
             self.c.setopt(pycurl.WRITEFUNCTION, self._write_body_callback)
             self.c.setopt(pycurl.HEADERFUNCTION, self._write_header_callback)
    +        self.c.setopt(pycurl.PREREQFUNCTION, self._pre_request_callback)
     
             self.log = getLogger(APPID)
     
    @@ -568,6 +571,16 @@ def _write_header_callback(self, buf):
             if self._header_buffer.endswith(b"\r\n\r\n"):
                 self.response_headers.parse(self._header_buffer)
     
    +    def _pre_request_callback(self, conn_primary_ip, conn_local_ip, conn_primary_port, conn_local_port):
    +        """
    +        Called after TCP/TLS connection is established, before request is sent.
    +        This runs for the initial request AND every redirect follow.
    +        """
    +        if not self.allow_private_ip and not is_global_address(conn_primary_ip):
    +            return pycurl.PREREQFUNC_ABORT  # Aborts the entire transfer
    +
    +        return pycurl.PREREQFUNC_OK
    +
         def add_header(self, name, value):
             """Append a value to a header name without replacing existing ones."""
             self.request_headers.add(name, value)
    
  • src/pyload/core/utils/web/check.py+1 1 modified
    @@ -3,7 +3,6 @@
     import re
     import time
     
    -from ...network.request_factory import get_url
     from .convert import host_to_ip
     
     
    @@ -137,6 +136,7 @@ def get_public_address(addr_type="ipv4"):
         - str: The public IPv4 or IPv6 address as returned by the external service, or
           an empty string if all attempts fail.
         """
    +    from ...network.request_factory import get_url
         if addr_type == "ipv4":
             services = [
                 ("https://ipv4.icanhazip.com/", r"(\S+)"),
    
  • src/pyload/plugins/base/downloader.py+1 10 modified
    @@ -6,8 +6,7 @@
     from pyload.core.network.exceptions import Fail
     from pyload.core.network.http.exceptions import BadHeader
     from pyload.core.utils import format, fs, parse
    -from pyload.core.utils.web.check import is_global_address, is_ip_address
    -from pyload.core.utils.web.convert import host_to_ip
    +from pyload.core.utils.web.check import is_global_host
     
     from ..helpers import exists
     from .hoster import BaseHoster
    @@ -332,14 +331,6 @@ def download(
             )
             self.check_status()
     
    -        dl_hostname = urllib.parse.urlparse(dl_url).hostname
    -        if is_ip_address(dl_hostname) and not is_global_address(dl_hostname):
    -            self.fail(self._("Refusing to download from Server-Side host {}".format(dl_hostname)))
    -        else:
    -            for ip in host_to_ip(dl_hostname):
    -                if not is_global_address(ip):
    -                    self.fail(self._("Refusing to download from Server-Side host {} ({})".format(dl_hostname, ip)))
    -
             newname = self._download(
                 dl_url, dl_filename, get, post, referrer, cookies, disposition, resume, chunks
             )
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.