VAITP Dataset

← Back to the dataset

CVE-2025-69226

AIOHTTP web.static() path normalization flaw reveals path component existence.

  • CVSS 6.3
  • CWE-22
  • Information Leakage
  • Remote

AIOHTTP is an asynchronous HTTP client/server framework for asyncio and Python. Versions 3.13.2 and below enable an attacker to ascertain the existence of absolute path components through the path normalization logic for static files meant to prevent path traversal. If an application uses web.static() (not recommended for production deployments), it may be possible for an attacker to ascertain the existence of path components. This issue is fixed in version 3.13.3.

CVSS base score
6.3
Published
2026-01-05
OWASP
A01 Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Incorrect Algorithm
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.3 or later.

Vulnerable code sample

import asyncio
from pathlib import Path
from aiohttp import web
import os
import shutil

# NOTE: This code is a representation of the logic that would be vulnerable
# to the described CVE. It requires aiohttp version 3.13.2 or older to
# exhibit the specific vulnerable behavior.

async def main():
    # 1. Setup a directory structure for the demonstration.
    # 'static_root' is the directory we intend to serve.
    static_root = Path("./www")
    static_root.mkdir(exist_ok=True)
    (static_root / "index.html").write_text("<html><body><h1>Public Files</h1></body></html>")

    # 2. Create a directory outside the web root to probe for.
    # This simulates a sensitive path that an attacker might try to find.
    sensitive_dir_outside_root = Path("./sensitive_system_data")
    sensitive_dir_outside_root.mkdir(exist_ok=True)

    # 3. Configure and run the aiohttp application.
    app = web.Application()

    # 4. The vulnerable configuration using web.static().
    # The path normalization and validation logic in affected versions allows
    # an attacker to distinguish between existing and non-existing absolute paths.
    # A request for `/static//sensitive_system_data/` would resolve to an
    # absolute path. The server would check if it exists, and because it does,
    # it would correctly deny access with a 403 Forbidden.
    # A request for `/static//non_existent_dir/` would also resolve to an
    # absolute path, but since it doesn't exist on the filesystem, the server
    # would proceed differently, ultimately resulting in a 404 Not Found.
    # This difference in status codes (403 vs 404) reveals the existence of
    # the 'sensitive_system_data' directory.
    app.router.add_static('/static/', path=str(static_root), show_index=True)

    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8080)
    await site.start()

    # Keep the server running to allow for testing.
    # The server can be stopped with Ctrl+C.
    while True:
        await asyncio.sleep(3600)

if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass
    finally:
        # Cleanup the created directories
        if Path("./www").exists():
            shutil.rmtree("./www")
        if Path("./sensitive_system_data").exists():
            shutil.rmtree("./sensitive_system_data")

Patched code sample

import pathlib
from aiohttp import web
import os

# This code demonstrates the principles used to fix path traversal and information
# disclosure vulnerabilities in aiohttp's static file handling, such as the one
# described in the user's query (conceptually similar to CVE-2024-23334).
#
# The vulnerability occurred because a URL path starting with a double slash (//)
# could be misinterpreted, causing the path to be treated as absolute instead of
# relative to the static directory root. This could allow an attacker to check for
# the existence of any file or directory on the system.
#
# The fix involves two main steps, highlighted in the code below:
# 1. Sanitize the requested filename to prevent it from being treated as an
#    absolute path.
# 2. After resolving the full path (including handling '..' and symlinks),
#    rigorously verify that the resulting path is still inside the intended
#    static root directory.


async def safe_static_file_handler(request: web.Request) -> web.StreamResponse:
    """
    A handler that safely serves files from a designated static directory,
    incorporating fixes for path traversal vulnerabilities.
    """
    static_root = pathlib.Path(__file__).parent.joinpath("static").resolve()
    
    # Get the raw requested filename from the URL
    raw_filename = request.match_info.get("filename", "")

    # --- FIX PART 1: Prevent the path from being treated as absolute ---
    # By stripping leading slashes, we ensure that the path component is always
    # treated as relative to the static_root. A request for "/static//etc/passwd"
    # will result in `filename_part` being "etc/passwd", not "/etc/passwd".
    # This prevents `pathlib.Path.joinpath` from discarding the static_root.
    filename_part = raw_filename.lstrip("/")

    try:
        # Construct the full, intended path
        file_path = static_root.joinpath(filename_part)

        # Resolve the path to get its canonical representation. This collapses
        # ".." components and follows symbolic links.
        resolved_path = file_path.resolve()

        # --- FIX PART 2: Verify the canonical path is within the root directory ---
        # This is the most critical check. It ensures that even after resolving ".."
        # or symlinks, the final path has not "escaped" the static directory.
        # The `relative_to()` method will raise a ValueError if `resolved_path`
        # is not a subpath of `static_root`.
        resolved_path.relative_to(static_root)

    except (ValueError, FileNotFoundError):
        # ValueError is raised by `relative_to` if the path is outside the root.
        # FileNotFoundError is raised by `resolve` if the path does not exist.
        # In either case, the attacker learns nothing and we return a 403 Forbidden
        # to indicate a disallowed request.
        raise web.HTTPForbidden()

    except Exception:
        # Catch any other potential exceptions and treat them as a forbidden request.
        raise web.HTTPForbidden()

    # If all checks pass and the path points to an actual file, serve it.
    if resolved_path.is_file():
        return web.FileResponse(resolved_path)

    # Do not serve directories or other non-file path types.
    raise web.HTTPForbidden()


def main() -> None:
    """Sets up and runs the web server."""
    # Create a dummy static directory and a file for demonstration
    static_dir = pathlib.Path(__file__).parent.joinpath("static")
    static_dir.mkdir(exist_ok=True)
    static_dir.joinpath("index.html").write_text("<h1>Hello, world!</h1>")
    print(f"Serving files from: {static_dir.resolve()}")
    print("Try accessing:")
    print("  - http://127.0.0.1:8080/static/index.html (should work)")
    print("  - http://127.0.0.1:8080/static/../../[some_system_file] (should be forbidden)")
    print("  - http://127.0.0.1:8080/static//etc/passwd (should be forbidden)")
    
    app = web.Application()
    app.add_routes([
        web.get("/static/{filename:.*}", safe_static_file_handler),
    ])
    web.run_app(app, host="127.0.0.1", port=8080)


if __name__ == "__main__":
    main()

Payload

/static/..%2fetc/

Cite this entry

@misc{vaitp:cve202569226,
  title        = {{AIOHTTP web.static() path normalization flaw reveals path component existence.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-69226},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-69226/}}
}
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 ::