File CVE-2024-27306.patch of Package python-aiohttp.40456
---
CHANGES/8317.bugfix.rst | 1
aiohttp/web_urldispatcher.py | 22 +++++--
tests/test_web_urldispatcher.py | 125 ++++++++++++++++++++++++++++++++++------
3 files changed, 126 insertions(+), 22 deletions(-)
Index: aiohttp-3.6.0/CHANGES/8317.bugfix.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ aiohttp-3.6.0/CHANGES/8317.bugfix.rst 2025-09-03 23:13:37.818761720 +0200
@@ -0,0 +1 @@
+Escaped filenames in static view -- by :user:`bdraco`.
Index: aiohttp-3.6.0/aiohttp/web_urldispatcher.py
===================================================================
--- aiohttp-3.6.0.orig/aiohttp/web_urldispatcher.py 2019-09-06 14:54:33.000000000 +0200
+++ aiohttp-3.6.0/aiohttp/web_urldispatcher.py 2025-09-03 23:13:37.819022859 +0200
@@ -1,7 +1,9 @@
import abc
import asyncio
import base64
+import functools
import hashlib
+import html
import inspect
import keyword
import os
@@ -32,7 +34,7 @@
cast,
)
-from yarl import URL
+from yarl import URL, __version__ as yarl_version
from . import hdrs
from .abc import AbstractMatchInfo, AbstractRouter, AbstractView
@@ -72,6 +74,8 @@
_ExpectHandler = Callable[[Request], Awaitable[None]]
_Resolve = Tuple[Optional[AbstractMatchInfo], Set[str]]
+html_escape = functools.partial(html.escape, quote=True)
+
class AbstractResource(Sized, Iterable['AbstractRoute']):
@@ -643,7 +647,7 @@
assert filepath.is_dir()
relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
- index_of = "Index of /{}".format(relative_path_to_dir)
+ index_of = "Index of /{}".format(html_escape(relative_path_to_dir))
h1 = "<h1>{}</h1>".format(index_of)
index_list = []
@@ -651,7 +655,7 @@
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("{}/{}".format(self._prefix, rel_path))
# if file is a directory, add '/' to the end of the name
if _file.is_dir():
@@ -660,8 +664,9 @@
file_name = _file.name
index_list.append(
- '<li><a href="{url}">{name}</a></li>'.format(url=file_url,
- name=file_name)
+ '<li><a href="{url}">{name}</a></li>'.format(
+ url=quoted_file_url, name=html_escape(file_name)
+ )
)
ul = "<ul>\n{}\n</ul>".format('\n'.join(index_list))
body = "<body>\n{}\n{}\n</body>".format(h1, ul)
@@ -1133,3 +1138,10 @@
"""
for route_def in routes:
route_def.register(self)
+
+
+def _quote_path(value: str):
+ YARL_VERSION = tuple(map(int, yarl_version.split(".")[:2]))
+ if YARL_VERSION < (1, 6):
+ value = value.replace("%", "%25")
+ return URL.build(path=value, encoded=False).raw_path
Index: aiohttp-3.6.0/tests/test_web_urldispatcher.py
===================================================================
--- aiohttp-3.6.0.orig/tests/test_web_urldispatcher.py 2019-09-06 14:54:33.000000000 +0200
+++ aiohttp-3.6.0/tests/test_web_urldispatcher.py 2025-09-03 23:13:37.819404476 +0200
@@ -1,6 +1,7 @@
import functools
import os
import pathlib
+import sys
import shutil
import tempfile
from unittest import mock
@@ -30,27 +31,30 @@
@pytest.mark.parametrize(
- "show_index,status,prefix,data",
- [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"),
- 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>',
- id="index_static")])
+ "show_index,status,prefix,request_path,data",
+ [pytest.param(False, 403, "/", "/", None, id="index_forbidden"),
+ pytest.param(True, 200, '/', "/",
+ 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="/my_dir">my_dir/</a></li>\n<li><a href="/my_file">'
+ b"my_file</a></li>\n</ul>\n</body>\n</html>"),
+ pytest.param(True, 200, '/static', "/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/my_dir">my_dir/</a></li>\n<li><a href="'
+ b'/static/my_file">my_file</a></li>\n</ul>\n</body>\n</html>',
+ id="index_static"),
+ pytest.param(True, 200, "/static", "/static/my_dir",
+ b"<html>\n<head>\n<title>Index of /my_dir</title>\n</head>\n<body>\n<h1>"
+ b'Index of /my_dir</h1>\n<ul>\n<li><a href="/static/my_dir/my_file_in_dir">'
+ b"my_file_in_dir</a></li>\n</ul>\n</body>\n</html>",
+ id="index_subdir"),
+ ]
+)
async def test_access_root_of_static_handler(tmp_dir_path,
aiohttp_client,
show_index,
status,
prefix,
+ request_path,
data) -> None:
"""
Tests the operation of static file server.
@@ -77,7 +81,7 @@
client = await aiohttp_client(app)
# Request the root of the static directory.
- r = await client.get(prefix)
+ r = await client.get(request_path)
assert r.status == status
if data:
@@ -86,6 +90,93 @@
assert read_ == data
+@pytest.mark.internal # Dependent on filesystem
+@pytest.mark.skipif(
+ not sys.platform.startswith("linux"),
+ reason="Invalid filenames on some filesystems (like Windows)",
+)
+@pytest.mark.parametrize(
+ "show_index,status,prefix,request_path,data",
+ [
+ pytest.param(False, 403, "/", "/", None, id="index_forbidden"),
+ pytest.param(
+ True,
+ 200,
+ "/",
+ "/",
+ 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)>.dir/</a></li>\n<li><a href="/%3Cimg%20sr'
+ b'c=0%20onerror=alert(1)%3E.txt"><img src=0 onerror=alert(1)>.txt</a></l'
+ b"i>\n</ul>\n</body>\n</html>",
+ ),
+ pytest.param(
+ True,
+ 200,
+ "/static",
+ "/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"><img src=0 onerror=alert(1)>.dir/</a></li>\n<li><a href="/stat'
+ b'ic/%3Cimg%20src=0%20onerror=alert(1)%3E.txt"><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 /<img src=0 onerror=alert(1)>.dir</t"
+ b"itle>\n</head>\n<body>\n<h1>Index of /<img src=0 onerror=alert(1)>.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_xss(
+ tmp_path,
+ aiohttp_client,
+ show_index,
+ status,
+ prefix,
+ request_path,
+ data,
+) -> 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.
+ # 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()
+ my_file_in_dir = my_dir / "my_file_in_dir"
+
+ with my_file.open("w") as fw:
+ fw.write("hello")
+
+ with my_file_in_dir.open("w") as fw:
+ fw.write("world")
+
+ app = web.Application()
+
+ # Register global static route:
+ app.router.add_static(prefix, str(tmp_path), show_index=show_index)
+ client = await aiohttp_client(app)
+
+ # Request the root of the static directory.
+ async with await client.get(request_path) as r:
+ assert r.status == status
+
+ if data:
+ assert r.headers['Content-Type'] == "text/html; charset=utf-8"
+ read_ = (await r.read())
+ assert read_ == data
+
+
async def test_follow_symlink(tmp_dir_path, aiohttp_client) -> None:
"""
Tests the access to a symlink, in static folder