VYPR
Moderate severityNVD Advisory· Published Aug 9, 2024· Updated Jun 9, 2025

In aiohttp, compressed files as symlinks are not protected from path traversal

CVE-2024-42367

Description

aiohttp is an asynchronous HTTP client/server framework for asyncio and Python. In versions on the 3.10 branch prior to version 3.10.2, static routes which contain files with compressed variants (.gz or .br extension) are vulnerable to path traversal outside the root directory if those variants are symbolic links. The server protects static routes from path traversal outside the root directory when follow_symlinks=False (default). It does this by resolving the requested URL to an absolute path and then checking that path relative to the root. However, these checks are not performed when looking for compressed variants in the FileResponse class, and symbolic links are then automatically followed when performing the Path.stat() and Path.open() to send the file. Version 3.10.2 contains a patch for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
aiohttpPyPI
>= 3.10.0b1, < 3.10.23.10.2

Affected products

1

Patches

1
ce2e97588145

[PR #8652/b0536ae6 backport][3.10] Do not follow symlinks for compressed file variants (#8653)

https://github.com/aio-libs/aiohttppatchback[bot]Aug 8, 2024via ghsa
4 files changed · +44 8
  • aiohttp/web_fileresponse.py+4 1 modified
    @@ -177,7 +177,10 @@ def _get_file_path_stat_encoding(
     
                 compressed_path = file_path.with_suffix(file_path.suffix + file_extension)
                 with suppress(OSError):
    -                return compressed_path, compressed_path.stat(), file_encoding
    +                # Do not follow symlinks and ignore any non-regular files.
    +                st = compressed_path.lstat()
    +                if S_ISREG(st.st_mode):
    +                    return compressed_path, st, file_encoding
     
             # Fallback to the uncompressed file
             return file_path, file_path.stat(), None
    
  • CHANGES/8652.bugfix.rst+1 0 added
    @@ -0,0 +1 @@
    +Fixed incorrectly following symlinks for compressed file variants -- by :user:`steverep`.
    
  • tests/test_web_sendfile.py+7 7 modified
    @@ -18,9 +18,9 @@ def test_using_gzip_if_header_present_and_file_available(loop) -> None:
         )
     
         gz_filepath = mock.create_autospec(Path, spec_set=True)
    -    gz_filepath.stat.return_value.st_size = 1024
    -    gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291
    -    gz_filepath.stat.return_value.st_mode = MOCK_MODE
    +    gz_filepath.lstat.return_value.st_size = 1024
    +    gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291
    +    gz_filepath.lstat.return_value.st_mode = MOCK_MODE
     
         filepath = mock.create_autospec(Path, spec_set=True)
         filepath.name = "logo.png"
    @@ -40,9 +40,9 @@ def test_gzip_if_header_not_present_and_file_available(loop) -> None:
         request = make_mocked_request("GET", "http://python.org/logo.png", headers={})
     
         gz_filepath = mock.create_autospec(Path, spec_set=True)
    -    gz_filepath.stat.return_value.st_size = 1024
    -    gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291
    -    gz_filepath.stat.return_value.st_mode = MOCK_MODE
    +    gz_filepath.lstat.return_value.st_size = 1024
    +    gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291
    +    gz_filepath.lstat.return_value.st_mode = MOCK_MODE
     
         filepath = mock.create_autospec(Path, spec_set=True)
         filepath.name = "logo.png"
    @@ -90,7 +90,7 @@ def test_gzip_if_header_present_and_file_not_available(loop) -> None:
         )
     
         gz_filepath = mock.create_autospec(Path, spec_set=True)
    -    gz_filepath.stat.side_effect = OSError(2, "No such file or directory")
    +    gz_filepath.lstat.side_effect = OSError(2, "No such file or directory")
     
         filepath = mock.create_autospec(Path, spec_set=True)
         filepath.name = "logo.png"
    
  • tests/test_web_urldispatcher.py+32 0 modified
    @@ -520,6 +520,38 @@ async def test_access_symlink_loop(
         assert r.status == 404
     
     
    +async def test_access_compressed_file_as_symlink(
    +    tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
    +) -> None:
    +    """Test that compressed file variants as symlinks are ignored."""
    +    private_file = tmp_path / "private.txt"
    +    private_file.write_text("private info")
    +    www_dir = tmp_path / "www"
    +    www_dir.mkdir()
    +    gz_link = www_dir / "file.txt.gz"
    +    gz_link.symlink_to(f"../{private_file.name}")
    +
    +    app = web.Application()
    +    app.router.add_static("/", www_dir)
    +    client = await aiohttp_client(app)
    +
    +    # Symlink should be ignored; response reflects missing uncompressed file.
    +    resp = await client.get(f"/{gz_link.stem}", auto_decompress=False)
    +    assert resp.status == 404
    +    resp.release()
    +
    +    # Again symlin is ignored, and then uncompressed is served.
    +    txt_file = gz_link.with_suffix("")
    +    txt_file.write_text("public data")
    +    resp = await client.get(f"/{txt_file.name}")
    +    assert resp.status == 200
    +    assert resp.headers.get("Content-Encoding") is None
    +    assert resp.content_type == "text/plain"
    +    assert await resp.text() == "public data"
    +    resp.release()
    +    await client.close()
    +
    +
     async def test_access_special_resource(
         tmp_path_factory: pytest.TempPathFactory, aiohttp_client: AiohttpClient
     ) -> None:
    

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

7

News mentions

0

No linked articles in our index yet.