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

Escape filenames and paths in HTML when generating index pages #8317

Merged
merged 10 commits into from
Apr 11, 2024
12 changes: 7 additions & 5 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import abc
import asyncio
import base64
import functools
import hashlib
import html
import keyword
import os
import re
Expand Down Expand Up @@ -87,6 +89,8 @@
_ExpectHandler = Callable[[Request], Awaitable[Optional[StreamResponse]]]
_Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]]

html_escape = functools.partial(html.escape, quote=True)


class _InfoDict(TypedDict, total=False):
path: str
Expand Down Expand Up @@ -686,15 +690,15 @@ def _directory_as_html(self, filepath: Path) -> str:
assert filepath.is_dir()

relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
index_of = f"Index of /{relative_path_to_dir}"
index_of = f"Index of /{html_escape(relative_path_to_dir)}"
h1 = f"<h1>{index_of}</h1>"

index_list = []
dir_index = filepath.iterdir()
for _file in sorted(dir_index):
# show file url as relative to static path
rel_path = _file.relative_to(self._directory).as_posix()
file_url = self._prefix + "/" + rel_path
quoted_file_url = _quote_path(f"{self._prefix}/{rel_path}")

# if file is a directory, add '/' to the end of the name
if _file.is_dir():
Expand All @@ -703,9 +707,7 @@ def _directory_as_html(self, filepath: Path) -> str:
file_name = _file.name

index_list.append(
'<li><a href="{url}">{name}</a></li>'.format(
url=file_url, name=file_name
)
f'<li><a href="{quoted_file_url}">{html_escape(file_name)}</a></li>'
)
ul = "<ul>\n{}\n</ul>".format("\n".join(index_list))
body = f"<body>\n{h1}\n{ul}\n</body>"
Expand Down
47 changes: 31 additions & 16 deletions tests/test_web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,43 @@


@pytest.mark.parametrize(
"show_index,status,prefix,data",
"show_index,status,prefix,request_path,data",
[
pytest.param(False, 403, "/", None, id="index_forbidden"),
pytest.param(False, 403, "/", "/", None, id="index_forbidden"),
pytest.param(
True,
200,
"/",
b"<html>\n<head>\n<title>Index of /.</title>\n"
b"</head>\n<body>\n<h1>Index of /.</h1>\n<ul>\n"
b'<li><a href="/my_dir">my_dir/</a></li>\n'
b'<li><a href="/my_file">my_file</a></li>\n'
b"</ul>\n</body>\n</html>",
id="index_root",
"/",
b"<html>\n<head>\n<title>Index of /.</title>\n</head>\n<body>\n<h1>Index of"
b' /.</h1>\n<ul>\n<li><a href="/%3Cimg%20src=0%20onerror=alert(1)%3E.dir">&l'
b't;img src=0 onerror=alert(1)&gt;.dir/</a></li>\n<li><a href="/%3Cimg%20sr'
b'c=0%20onerror=alert(1)%3E.txt">&lt;img src=0 onerror=alert(1)&gt;.txt</a></l'
b"i>\n</ul>\n</body>\n</html>",
),
pytest.param(
True,
200,
"/static",
b"<html>\n<head>\n<title>Index of /.</title>\n"
b"</head>\n<body>\n<h1>Index of /.</h1>\n<ul>\n"
b'<li><a href="/static/my_dir">my_dir/</a></li>\n'
b'<li><a href="/static/my_file">my_file</a></li>\n'
b"</ul>\n</body>\n</html>",
"/static",
b"<html>\n<head>\n<title>Index of /.</title>\n</head>\n<body>\n<h1>Index of"
b' /.</h1>\n<ul>\n<li><a href="/static/%3Cimg%20src=0%20onerror=alert(1)%3E.'
b'dir">&lt;img src=0 onerror=alert(1)&gt;.dir/</a></li>\n<li><a href="/stat'
b'ic/%3Cimg%20src=0%20onerror=alert(1)%3E.txt">&lt;img src=0 onerror=alert(1)&'
b"gt;.txt</a></li>\n</ul>\n</body>\n</html>",
id="index_static",
),
pytest.param(
True,
200,
"/static",
"/static/<img src=0 onerror=alert(1)>.dir",
b"<html>\n<head>\n<title>Index of /&lt;img src=0 onerror=alert(1)&gt;.dir</t"
b"itle>\n</head>\n<body>\n<h1>Index of /&lt;img src=0 onerror=alert(1)&gt;.di"
b'r</h1>\n<ul>\n<li><a href="/static/%3Cimg%20src=0%20onerror=alert(1)%3E.di'
b'r/my_file_in_dir">my_file_in_dir</a></li>\n</ul>\n</body>\n</html>',
id="index_subdir",
),
],
)
async def test_access_root_of_static_handler(
Expand All @@ -47,14 +59,17 @@ async def test_access_root_of_static_handler(
show_index: bool,
status: int,
prefix: str,
request_path: str,
data: Optional[bytes],
) -> None:
# Tests the operation of static file server.
# Try to access the root of static file server, and make
# sure that correct HTTP statuses are returned depending if we directory
# index should be shown or not.
my_file = tmp_path / "my_file"
my_dir = tmp_path / "my_dir"
# Ensure that html in file names is escaped.
# Ensure that links are url quoted.
my_file = tmp_path / "<img src=0 onerror=alert(1)>.txt"
my_dir = tmp_path / "<img src=0 onerror=alert(1)>.dir"
my_dir.mkdir()
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
my_file_in_dir = my_dir / "my_file_in_dir"

Expand All @@ -71,7 +86,7 @@ async def test_access_root_of_static_handler(
client = await aiohttp_client(app)

# Request the root of the static directory.
async with await client.get(prefix) as r:
async with await client.get(request_path) as r:
assert r.status == status

if data:
Expand Down
Loading