Skip to content

Commit ce2e975

Browse files
[PR #8652/b0536ae6 backport][3.10] Do not follow symlinks for compressed file variants (#8653)
Co-authored-by: J. Nick Koston <[email protected]>
1 parent 6a77806 commit ce2e975

File tree

4 files changed

+44
-8
lines changed

4 files changed

+44
-8
lines changed

CHANGES/8652.bugfix.rst

+1
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

+4-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ def _get_file_path_stat_encoding(
177177

178178
compressed_path = file_path.with_suffix(file_path.suffix + file_extension)
179179
with suppress(OSError):
180-
return compressed_path, compressed_path.stat(), file_encoding
180+
# Do not follow symlinks and ignore any non-regular files.
181+
st = compressed_path.lstat()
182+
if S_ISREG(st.st_mode):
183+
return compressed_path, st, file_encoding
181184

182185
# Fallback to the uncompressed file
183186
return file_path, file_path.stat(), None

tests/test_web_sendfile.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def test_using_gzip_if_header_present_and_file_available(loop) -> None:
1818
)
1919

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

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

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

4747
filepath = mock.create_autospec(Path, spec_set=True)
4848
filepath.name = "logo.png"
@@ -90,7 +90,7 @@ def test_gzip_if_header_present_and_file_not_available(loop) -> None:
9090
)
9191

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

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

tests/test_web_urldispatcher.py

+32
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,38 @@ async def test_access_symlink_loop(
520520
assert r.status == 404
521521

522522

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

0 commit comments

Comments
 (0)