Skip to content

Commit b0536ae

Browse files
bdracosteverep
andauthored
Do not follow symlinks for compressed file variants (#8652)
Co-authored-by: Steve Repsher <[email protected]>
1 parent 51d872e commit b0536ae

File tree

4 files changed

+44
-8
lines changed

4 files changed

+44
-8
lines changed

CHANGES/8652.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed incorrectly following symlinks for compressed file variants -- by :user:`steverep`.

aiohttp/web_fileresponse.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ def _get_file_path_stat_encoding(
174174

175175
compressed_path = file_path.with_suffix(file_path.suffix + file_extension)
176176
with suppress(OSError):
177-
return compressed_path, compressed_path.stat(), file_encoding
177+
# Do not follow symlinks and ignore any non-regular files.
178+
st = compressed_path.lstat()
179+
if S_ISREG(st.st_mode):
180+
return compressed_path, st, file_encoding
178181

179182
# Fallback to the uncompressed file
180183
return file_path, file_path.stat(), None

tests/test_web_sendfile.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def test_using_gzip_if_header_present_and_file_available(loop: Any) -> None:
1919
)
2020

2121
gz_filepath = mock.create_autospec(Path, spec_set=True)
22-
gz_filepath.stat.return_value.st_size = 1024
23-
gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291
24-
gz_filepath.stat.return_value.st_mode = MOCK_MODE
22+
gz_filepath.lstat.return_value.st_size = 1024
23+
gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291
24+
gz_filepath.lstat.return_value.st_mode = MOCK_MODE
2525

2626
filepath = mock.create_autospec(Path, spec_set=True)
2727
filepath.name = "logo.png"
@@ -41,9 +41,9 @@ def test_gzip_if_header_not_present_and_file_available(loop: Any) -> None:
4141
request = make_mocked_request("GET", "http://python.org/logo.png", headers={})
4242

4343
gz_filepath = mock.create_autospec(Path, spec_set=True)
44-
gz_filepath.stat.return_value.st_size = 1024
45-
gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291
46-
gz_filepath.stat.return_value.st_mode = MOCK_MODE
44+
gz_filepath.lstat.return_value.st_size = 1024
45+
gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291
46+
gz_filepath.lstat.return_value.st_mode = MOCK_MODE
4747

4848
filepath = mock.create_autospec(Path, spec_set=True)
4949
filepath.name = "logo.png"
@@ -91,7 +91,7 @@ def test_gzip_if_header_present_and_file_not_available(loop: Any) -> None:
9191
)
9292

9393
gz_filepath = mock.create_autospec(Path, spec_set=True)
94-
gz_filepath.stat.side_effect = OSError(2, "No such file or directory")
94+
gz_filepath.lstat.side_effect = OSError(2, "No such file or directory")
9595

9696
filepath = mock.create_autospec(Path, spec_set=True)
9797
filepath.name = "logo.png"

tests/test_web_urldispatcher.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,38 @@ async def test_access_symlink_loop(
514514
assert r.status == 404
515515

516516

517+
async def test_access_compressed_file_as_symlink(
518+
tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
519+
) -> None:
520+
"""Test that compressed file variants as symlinks are ignored."""
521+
private_file = tmp_path / "private.txt"
522+
private_file.write_text("private info")
523+
www_dir = tmp_path / "www"
524+
www_dir.mkdir()
525+
gz_link = www_dir / "file.txt.gz"
526+
gz_link.symlink_to(f"../{private_file.name}")
527+
528+
app = web.Application()
529+
app.router.add_static("/", www_dir)
530+
client = await aiohttp_client(app)
531+
532+
# Symlink should be ignored; response reflects missing uncompressed file.
533+
resp = await client.get(f"/{gz_link.stem}", auto_decompress=False)
534+
assert resp.status == 404
535+
resp.release()
536+
537+
# Again symlin is ignored, and then uncompressed is served.
538+
txt_file = gz_link.with_suffix("")
539+
txt_file.write_text("public data")
540+
resp = await client.get(f"/{txt_file.name}")
541+
assert resp.status == 200
542+
assert resp.headers.get("Content-Encoding") is None
543+
assert resp.content_type == "text/plain"
544+
assert await resp.text() == "public data"
545+
resp.release()
546+
await client.close()
547+
548+
517549
async def test_access_special_resource(
518550
tmp_path_factory: pytest.TempPathFactory, aiohttp_client: AiohttpClient
519551
) -> None:

0 commit comments

Comments
 (0)