Skip to content

Commit

Permalink
Do not follow symlinks for compressed file variants (#8652)
Browse files Browse the repository at this point in the history
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
  • Loading branch information
bdraco and steverep authored Aug 8, 2024
1 parent 51d872e commit b0536ae
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGES/8652.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed incorrectly following symlinks for compressed file variants -- by :user:`steverep`.
5 changes: 4 additions & 1 deletion aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,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
Expand Down
14 changes: 7 additions & 7 deletions tests/test_web_sendfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def test_using_gzip_if_header_present_and_file_available(loop: Any) -> 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"
Expand All @@ -41,9 +41,9 @@ def test_gzip_if_header_not_present_and_file_available(loop: Any) -> 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"
Expand Down Expand Up @@ -91,7 +91,7 @@ def test_gzip_if_header_present_and_file_not_available(loop: Any) -> 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"
Expand Down
32 changes: 32 additions & 0 deletions tests/test_web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,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:
Expand Down

0 comments on commit b0536ae

Please sign in to comment.