Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
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 @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
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 @@ -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:
Expand Down
Loading