VYPR
Low severityOSV Advisory· Published Jan 5, 2026· Updated Jan 6, 2026

AIOHTTP allows for a brute-force leak of internal static filepath components

CVE-2025-69226

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
aiohttpPyPI
< 3.13.33.13.3

Affected products

1

Patches

1
f2a86fd5ac03

Reject static URLs that traverse outside static root (#11888) (#11906)

https://github.com/aio-libs/aiohttpSam BullJan 3, 2026via ghsa
4 files changed · +29 13
  • aiohttp/web_urldispatcher.py+9 9 modified
    @@ -7,6 +7,7 @@
     import inspect
     import keyword
     import os
    +import platform
     import re
     import sys
     import warnings
    @@ -94,6 +95,7 @@
     )
     PATH_SEP: Final[str] = re.escape("/")
     
    +IS_WINDOWS: Final[bool] = platform.system() == "Windows"
     
     _ExpectHandler = Callable[[Request], Awaitable[Optional[StreamResponse]]]
     _Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]]
    @@ -651,7 +653,12 @@ def set_options_route(self, handler: Handler) -> None:
         async def resolve(self, request: Request) -> _Resolve:
             path = request.rel_url.path_safe
             method = request.method
    -        if not path.startswith(self._prefix2) and path != self._prefix:
    +        # We normalise here to avoid matches that traverse below the static root.
    +        # e.g. /static/../../../../home/user/webapp/static/
    +        norm_path = os.path.normpath(path)
    +        if IS_WINDOWS:
    +            norm_path = norm_path.replace("\\", "/")
    +        if not norm_path.startswith(self._prefix2) and norm_path != self._prefix:
                 return None, set()
     
             allowed_methods = self._allowed_methods
    @@ -668,14 +675,7 @@ def __iter__(self) -> Iterator[AbstractRoute]:
             return iter(self._routes.values())
     
         async def _handle(self, request: Request) -> StreamResponse:
    -        rel_url = request.match_info["filename"]
    -        filename = Path(rel_url)
    -        if filename.anchor:
    -            # rel_url is an absolute name like
    -            # /static/\\machine_name\c$ or /static/D:\path
    -            # where the static dir is totally different
    -            raise HTTPForbidden()
    -
    +        filename = request.match_info["filename"]
             unresolved_path = self._directory.joinpath(filename)
             loop = asyncio.get_running_loop()
             return await loop.run_in_executor(
    
  • tests/test_urldispatch.py+17 1 modified
    @@ -1,4 +1,5 @@
     import pathlib
    +import platform
     import re
     from collections.abc import Container, Iterable, Mapping, MutableMapping, Sized
     from typing import NoReturn
    @@ -1041,7 +1042,22 @@ async def test_405_for_resource_adapter(router) -> None:
         assert (None, {"HEAD", "GET"}) == ret
     
     
    -async def test_check_allowed_method_for_found_resource(router) -> None:
    +@pytest.mark.skipif(platform.system() == "Windows", reason="Different path formats")
    +async def test_static_resource_outside_traversal(router: web.UrlDispatcher) -> None:
    +    """Test relative path traversing outside root does not resolve."""
    +    static_file = pathlib.Path(aiohttp.__file__)
    +    request_path = "/st" + "/.." * (len(static_file.parts) - 2) + str(static_file)
    +    assert pathlib.Path(request_path).resolve() == static_file
    +
    +    resource = router.add_static("/st", static_file.parent)
    +    ret = await resource.resolve(make_mocked_request("GET", request_path))
    +    # Should not resolve, otherwise filesystem information may be leaked.
    +    assert (None, set()) == ret
    +
    +
    +async def test_check_allowed_method_for_found_resource(
    +    router: web.UrlDispatcher,
    +) -> None:
         handler = make_handler()
         resource = router.add_resource("/")
         resource.add_route("GET", handler)
    
  • tests/test_web_sendfile_functional.py+1 1 modified
    @@ -638,7 +638,7 @@ async def test_static_file_directory_traversal_attack(aiohttp_client) -> None:
     
         url_abspath = "/static/" + str(full_path.resolve())
         resp = await client.get(url_abspath)
    -    assert 403 == resp.status
    +    assert resp.status == 404
         await resp.release()
     
         await client.close()
    
  • tests/test_web_urldispatcher.py+2 2 modified
    @@ -858,8 +858,8 @@ async def test_static_absolute_url(
         here = pathlib.Path(__file__).parent
         app.router.add_static("/static", here)
         client = await aiohttp_client(app)
    -    resp = await client.get("/static/" + str(file_path.resolve()))
    -    assert resp.status == 403
    +    async with client.get("/static/" + str(file_path.resolve())) as resp:
    +        assert resp.status == 404
     
     
     async def test_for_issue_5250(
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.