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.
- CWE
- CWE-918
- 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 ::
