diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-aiohttp')
5 files changed, 850 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49081.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49081.patch new file mode 100644 index 0000000000..503b001445 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49081.patch | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | From 67bf97cd1dfa513c8b6374905ee225b4d46cdf20 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Mon, 13 Nov 2023 22:13:06 +0000 | ||
| 4 | Subject: [PATCH] Disallow arbitrary sequence types in version (#7835) | ||
| 5 | |||
| 6 | Upstream-Status: Backport | ||
| 7 | [https://github.com/aio-libs/aiohttp/commit/1e86b777e61cf4eefc7d92fa57fa19dcc676013b] | ||
| 8 | |||
| 9 | CVE: CVE-2023-49081 | ||
| 10 | |||
| 11 | Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> | ||
| 12 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 13 | --- | ||
| 14 | CHANGES/7835.bugfix | 1 + | ||
| 15 | aiohttp/client_reqrep.py | 4 ++-- | ||
| 16 | tests/test_client_request.py | 18 +++++++++++++++--- | ||
| 17 | 3 files changed, 18 insertions(+), 5 deletions(-) | ||
| 18 | create mode 100644 CHANGES/7835.bugfix | ||
| 19 | |||
| 20 | diff --git a/CHANGES/7835.bugfix b/CHANGES/7835.bugfix | ||
| 21 | new file mode 100644 | ||
| 22 | index 0000000..4ce3af4 | ||
| 23 | --- /dev/null | ||
| 24 | +++ b/CHANGES/7835.bugfix | ||
| 25 | @@ -0,0 +1 @@ | ||
| 26 | +Fixed arbitrary sequence types being allowed to inject headers via version parameter -- by :user:`Dreamsorcerer` | ||
| 27 | diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py | ||
| 28 | index 987d68f..d3cd77e 100644 | ||
| 29 | --- a/aiohttp/client_reqrep.py | ||
| 30 | +++ b/aiohttp/client_reqrep.py | ||
| 31 | @@ -661,8 +661,8 @@ class ClientRequest: | ||
| 32 | self.headers[hdrs.CONNECTION] = connection | ||
| 33 | |||
| 34 | # status + headers | ||
| 35 | - status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format( | ||
| 36 | - self.method, path, self.version | ||
| 37 | + status_line = "{0} {1} HTTP/{v.major}.{v.minor}".format( | ||
| 38 | + self.method, path, v=self.version | ||
| 39 | ) | ||
| 40 | await writer.write_headers(status_line, self.headers) | ||
| 41 | |||
| 42 | diff --git a/tests/test_client_request.py b/tests/test_client_request.py | ||
| 43 | index 9eeb933..009f1a0 100644 | ||
| 44 | --- a/tests/test_client_request.py | ||
| 45 | +++ b/tests/test_client_request.py | ||
| 46 | @@ -20,6 +20,7 @@ from aiohttp.client_reqrep import ( | ||
| 47 | _merge_ssl_params, | ||
| 48 | ) | ||
| 49 | from aiohttp.helpers import PY_311 | ||
| 50 | +from aiohttp.http import HttpVersion | ||
| 51 | from aiohttp.test_utils import make_mocked_coro | ||
| 52 | |||
| 53 | |||
| 54 | @@ -576,18 +577,18 @@ async def test_connection_header(loop, conn) -> None: | ||
| 55 | req.headers.clear() | ||
| 56 | |||
| 57 | req.keep_alive.return_value = True | ||
| 58 | - req.version = (1, 1) | ||
| 59 | + req.version = HttpVersion(1, 1) | ||
| 60 | req.headers.clear() | ||
| 61 | await req.send(conn) | ||
| 62 | assert req.headers.get("CONNECTION") is None | ||
| 63 | |||
| 64 | - req.version = (1, 0) | ||
| 65 | + req.version = HttpVersion(1, 0) | ||
| 66 | req.headers.clear() | ||
| 67 | await req.send(conn) | ||
| 68 | assert req.headers.get("CONNECTION") == "keep-alive" | ||
| 69 | |||
| 70 | req.keep_alive.return_value = False | ||
| 71 | - req.version = (1, 1) | ||
| 72 | + req.version = HttpVersion(1, 1) | ||
| 73 | req.headers.clear() | ||
| 74 | await req.send(conn) | ||
| 75 | assert req.headers.get("CONNECTION") == "close" | ||
| 76 | @@ -1113,6 +1114,17 @@ async def test_close(loop, buf, conn) -> None: | ||
| 77 | await req.close() | ||
| 78 | resp.close() | ||
| 79 | |||
| 80 | +async def test_bad_version(loop: Any, conn: Any) -> None: | ||
| 81 | + req = ClientRequest( | ||
| 82 | + "GET", | ||
| 83 | + URL("http://python.org"), | ||
| 84 | + loop=loop, | ||
| 85 | + headers={"Connection": "Close"}, | ||
| 86 | + version=("1", "1\r\nInjected-Header: not allowed"), | ||
| 87 | + ) | ||
| 88 | + | ||
| 89 | + with pytest.raises(AttributeError): | ||
| 90 | + await req.send(conn) | ||
| 91 | |||
| 92 | async def test_custom_response_class(loop, conn) -> None: | ||
| 93 | class CustomResponse(ClientResponse): | ||
| 94 | -- | ||
| 95 | 2.25.1 | ||
| 96 | |||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49082.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49082.patch new file mode 100644 index 0000000000..cfcb980317 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2023-49082.patch | |||
| @@ -0,0 +1,105 @@ | |||
| 1 | From a2200dc43d9fe0ee19b9185b30749c204a4dfd45 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Wed, 8 Nov 2023 19:25:05 +0000 | ||
| 4 | Subject: [PATCH] Add HTTP method validation (#6533) (#7806) | ||
| 5 | |||
| 6 | (cherry picked from commit 75fca0b00b4297d0a30c51ae97a65428336eb2c1) | ||
| 7 | |||
| 8 | Upstream-Status: Backport | ||
| 9 | [https://github.com/aio-libs/aiohttp/pull/7806/commits/a43bc1779892e7014b7723c59d08fb37a000955e] | ||
| 10 | |||
| 11 | CVE: CVE-2023-49082 | ||
| 12 | |||
| 13 | Co-authored-by: Andrew Svetlov <andrew.svetlov@gmail.com> | ||
| 14 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 15 | --- | ||
| 16 | CHANGES/6533.feature | 1 + | ||
| 17 | aiohttp/client_reqrep.py | 9 ++++++++- | ||
| 18 | tests/test_client_request.py | 5 +++++ | ||
| 19 | tests/test_web_request.py | 9 +++++++-- | ||
| 20 | 4 files changed, 21 insertions(+), 3 deletions(-) | ||
| 21 | create mode 100644 CHANGES/6533.feature | ||
| 22 | |||
| 23 | diff --git a/CHANGES/6533.feature b/CHANGES/6533.feature | ||
| 24 | new file mode 100644 | ||
| 25 | index 0000000..36bcbeb | ||
| 26 | --- /dev/null | ||
| 27 | +++ b/CHANGES/6533.feature | ||
| 28 | @@ -0,0 +1 @@ | ||
| 29 | +Add HTTP method validation. | ||
| 30 | diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py | ||
| 31 | index d3cd77e..a8135b2 100644 | ||
| 32 | --- a/aiohttp/client_reqrep.py | ||
| 33 | +++ b/aiohttp/client_reqrep.py | ||
| 34 | @@ -78,6 +78,7 @@ if TYPE_CHECKING: # pragma: no cover | ||
| 35 | from .tracing import Trace | ||
| 36 | |||
| 37 | |||
| 38 | +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") | ||
| 39 | json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json") | ||
| 40 | |||
| 41 | |||
| 42 | @@ -266,10 +267,16 @@ class ClientRequest: | ||
| 43 | proxy_headers: Optional[LooseHeaders] = None, | ||
| 44 | traces: Optional[List["Trace"]] = None, | ||
| 45 | ): | ||
| 46 | - | ||
| 47 | if loop is None: | ||
| 48 | loop = asyncio.get_event_loop() | ||
| 49 | |||
| 50 | + match = _CONTAINS_CONTROL_CHAR_RE.search(method) | ||
| 51 | + if match: | ||
| 52 | + raise ValueError( | ||
| 53 | + f"Method cannot contain non-token characters {method!r} " | ||
| 54 | + "(found at least {match.group()!r})" | ||
| 55 | + ) | ||
| 56 | + | ||
| 57 | assert isinstance(url, URL), url | ||
| 58 | assert isinstance(proxy, (URL, type(None))), proxy | ||
| 59 | # FIXME: session is None in tests only, need to fix tests | ||
| 60 | diff --git a/tests/test_client_request.py b/tests/test_client_request.py | ||
| 61 | index 009f1a0..d0f208b 100644 | ||
| 62 | --- a/tests/test_client_request.py | ||
| 63 | +++ b/tests/test_client_request.py | ||
| 64 | @@ -89,6 +89,11 @@ def test_method3(make_request) -> None: | ||
| 65 | assert req.method == "HEAD" | ||
| 66 | |||
| 67 | |||
| 68 | +def test_method_invalid(make_request) -> None: | ||
| 69 | + with pytest.raises(ValueError, match="Method cannot contain non-token characters"): | ||
| 70 | + make_request("METHOD WITH\nWHITESPACES", "http://python.org/") | ||
| 71 | + | ||
| 72 | + | ||
| 73 | def test_version_1_0(make_request) -> None: | ||
| 74 | req = make_request("get", "http://python.org/", version="1.0") | ||
| 75 | assert req.version == (1, 0) | ||
| 76 | diff --git a/tests/test_web_request.py b/tests/test_web_request.py | ||
| 77 | index c6aeaf8..2bb0cd5 100644 | ||
| 78 | --- a/tests/test_web_request.py | ||
| 79 | +++ b/tests/test_web_request.py | ||
| 80 | @@ -43,7 +43,10 @@ def test_base_ctor() -> None: | ||
| 81 | |||
| 82 | assert "GET" == req.method | ||
| 83 | assert HttpVersion(1, 1) == req.version | ||
| 84 | - assert req.host == socket.getfqdn() | ||
| 85 | + # MacOS may return CamelCased host name, need .lower() | ||
| 86 | + # FQDN can be wider than host, e.g. | ||
| 87 | + # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' | ||
| 88 | + assert req.host.lower() in socket.getfqdn().lower() | ||
| 89 | assert "/path/to?a=1&b=2" == req.path_qs | ||
| 90 | assert "/path/to" == req.path | ||
| 91 | assert "a=1&b=2" == req.query_string | ||
| 92 | @@ -66,7 +69,9 @@ def test_ctor() -> None: | ||
| 93 | assert "GET" == req.method | ||
| 94 | assert HttpVersion(1, 1) == req.version | ||
| 95 | # MacOS may return CamelCased host name, need .lower() | ||
| 96 | - assert req.host.lower() == socket.getfqdn().lower() | ||
| 97 | + # FQDN can be wider than host, e.g. | ||
| 98 | + # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net' | ||
| 99 | + assert req.host.lower() in socket.getfqdn().lower() | ||
| 100 | assert "/path/to?a=1&b=2" == req.path_qs | ||
| 101 | assert "/path/to" == req.path | ||
| 102 | assert "a=1&b=2" == req.query_string | ||
| 103 | -- | ||
| 104 | 2.25.1 | ||
| 105 | |||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-27306.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-27306.patch new file mode 100644 index 0000000000..f87ef92679 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-27306.patch | |||
| @@ -0,0 +1,81 @@ | |||
| 1 | From d05042f1a35ec0adb797c056024d457ac1fd7088 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Thu, 11 Apr 2024 15:54:45 +0100 | ||
| 4 | Subject: [PATCH] Escape filenames and paths in HTML when generating index | ||
| 5 | pages (#8317) (#8319) | ||
| 6 | |||
| 7 | Upstream-Status: Backport | ||
| 8 | [https://github.com/aio-libs/aiohttp/commit/28335525d1eac015a7e7584137678cbb6ff19397] | ||
| 9 | |||
| 10 | CVE: CVE-2024-27306 | ||
| 11 | |||
| 12 | Co-authored-by: J. Nick Koston <nick@koston.org> | ||
| 13 | (cherry picked from commit ffbc43233209df302863712b511a11bdb6001b0f) | ||
| 14 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 15 | --- | ||
| 16 | CHANGES/8317.bugfix.rst | 1 + | ||
| 17 | aiohttp/web_urldispatcher.py | 11 ++++++----- | ||
| 18 | 2 files changed, 7 insertions(+), 5 deletions(-) | ||
| 19 | create mode 100644 CHANGES/8317.bugfix.rst | ||
| 20 | |||
| 21 | diff --git a/CHANGES/8317.bugfix.rst b/CHANGES/8317.bugfix.rst | ||
| 22 | new file mode 100644 | ||
| 23 | index 0000000..b24ef2a | ||
| 24 | --- /dev/null | ||
| 25 | +++ b/CHANGES/8317.bugfix.rst | ||
| 26 | @@ -0,0 +1 @@ | ||
| 27 | +Escaped filenames in static view -- by :user:`bdraco`. | ||
| 28 | diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py | ||
| 29 | index e8a8023..791ab94 100644 | ||
| 30 | --- a/aiohttp/web_urldispatcher.py | ||
| 31 | +++ b/aiohttp/web_urldispatcher.py | ||
| 32 | @@ -1,7 +1,9 @@ | ||
| 33 | import abc | ||
| 34 | import asyncio | ||
| 35 | import base64 | ||
| 36 | +import functools | ||
| 37 | import hashlib | ||
| 38 | +import html | ||
| 39 | import inspect | ||
| 40 | import keyword | ||
| 41 | import os | ||
| 42 | @@ -87,6 +89,7 @@ PATH_SEP: Final[str] = re.escape("/") | ||
| 43 | _ExpectHandler = Callable[[Request], Awaitable[None]] | ||
| 44 | _Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]] | ||
| 45 | |||
| 46 | +html_escape = functools.partial(html.escape, quote=True) | ||
| 47 | |||
| 48 | class _InfoDict(TypedDict, total=False): | ||
| 49 | path: str | ||
| 50 | @@ -706,7 +709,7 @@ class StaticResource(PrefixResource): | ||
| 51 | assert filepath.is_dir() | ||
| 52 | |||
| 53 | relative_path_to_dir = filepath.relative_to(self._directory).as_posix() | ||
| 54 | - index_of = f"Index of /{relative_path_to_dir}" | ||
| 55 | + index_of = f"Index of /{html_escape(relative_path_to_dir)}" | ||
| 56 | h1 = f"<h1>{index_of}</h1>" | ||
| 57 | |||
| 58 | index_list = [] | ||
| 59 | @@ -714,7 +717,7 @@ class StaticResource(PrefixResource): | ||
| 60 | for _file in sorted(dir_index): | ||
| 61 | # show file url as relative to static path | ||
| 62 | rel_path = _file.relative_to(self._directory).as_posix() | ||
| 63 | - file_url = self._prefix + "/" + rel_path | ||
| 64 | + quoted_file_url = _quote_path(f"{self._prefix}/{rel_path}") | ||
| 65 | |||
| 66 | # if file is a directory, add '/' to the end of the name | ||
| 67 | if _file.is_dir(): | ||
| 68 | @@ -723,9 +726,7 @@ class StaticResource(PrefixResource): | ||
| 69 | file_name = _file.name | ||
| 70 | |||
| 71 | index_list.append( | ||
| 72 | - '<li><a href="{url}">{name}</a></li>'.format( | ||
| 73 | - url=file_url, name=file_name | ||
| 74 | - ) | ||
| 75 | + f'<li><a href="{quoted_file_url}">{html_escape(file_name)}</a></li>' | ||
| 76 | ) | ||
| 77 | ul = "<ul>\n{}\n</ul>".format("\n".join(index_list)) | ||
| 78 | body = f"<body>\n{h1}\n{ul}\n</body>" | ||
| 79 | -- | ||
| 80 | 2.25.1 | ||
| 81 | |||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch new file mode 100644 index 0000000000..20226eb754 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-30251.patch | |||
| @@ -0,0 +1,522 @@ | |||
| 1 | From 44108afc0c0460d154216cab9aaa1d8f57edc3cc Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Sun, 7 Apr 2024 13:19:31 +0100 | ||
| 4 | Subject: [PATCH] Fix handling of multipart/form-data (#8280) (#8302) | ||
| 5 | |||
| 6 | https://datatracker.ietf.org/doc/html/rfc7578 | ||
| 7 | (cherry picked from commit 7d0be3fee540a3d4161ac7dc76422f1f5ea60104) | ||
| 8 | |||
| 9 | The following commits are also included: | ||
| 10 | 7eecdff1 [PR #8332/482e6cdf backport][3.9] Add set_content_disposition test (#8333) | ||
| 11 | f21c6f2c [PR #8335/5a6949da backport][3.9] Add Content-Disposition automatically (#8336) | ||
| 12 | |||
| 13 | Upstream-Status: Backport | ||
| 14 | [https://github.com/aio-libs/aiohttp/commit/cebe526b9c34dc3a3da9140409db63014bc4cf19 | ||
| 15 | https://github.com/aio-libs/aiohttp/commit/7eecdff163ccf029fbb1ddc9de4169d4aaeb6597 | ||
| 16 | https://github.com/aio-libs/aiohttp/commit/f21c6f2ca512a026ce7f0f6c6311f62d6a638866] | ||
| 17 | |||
| 18 | CVE: CVE-2024-30251 | ||
| 19 | |||
| 20 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 21 | --- | ||
| 22 | CHANGES/8280.bugfix.rst | 1 + | ||
| 23 | CHANGES/8280.deprecation.rst | 2 + | ||
| 24 | CHANGES/8332.bugfix.rst | 1 + | ||
| 25 | CHANGES/8335.bugfix.rst | 1 + | ||
| 26 | aiohttp/formdata.py | 12 +++- | ||
| 27 | aiohttp/multipart.py | 128 ++++++++++++++++++++++++----------- | ||
| 28 | tests/test_multipart.py | 87 ++++++++++++++++++++---- | ||
| 29 | tests/test_web_functional.py | 27 ++------ | ||
| 30 | 8 files changed, 182 insertions(+), 77 deletions(-) | ||
| 31 | create mode 100644 CHANGES/8280.bugfix.rst | ||
| 32 | create mode 100644 CHANGES/8280.deprecation.rst | ||
| 33 | create mode 100644 CHANGES/8332.bugfix.rst | ||
| 34 | create mode 100644 CHANGES/8335.bugfix.rst | ||
| 35 | |||
| 36 | diff --git a/CHANGES/8280.bugfix.rst b/CHANGES/8280.bugfix.rst | ||
| 37 | new file mode 100644 | ||
| 38 | index 0000000..3aebe36 | ||
| 39 | --- /dev/null | ||
| 40 | +++ b/CHANGES/8280.bugfix.rst | ||
| 41 | @@ -0,0 +1 @@ | ||
| 42 | +Fixed ``multipart/form-data`` compliance with :rfc:`7578` -- by :user:`Dreamsorcerer`. | ||
| 43 | diff --git a/CHANGES/8280.deprecation.rst b/CHANGES/8280.deprecation.rst | ||
| 44 | new file mode 100644 | ||
| 45 | index 0000000..302dbb2 | ||
| 46 | --- /dev/null | ||
| 47 | +++ b/CHANGES/8280.deprecation.rst | ||
| 48 | @@ -0,0 +1,2 @@ | ||
| 49 | +Deprecated ``content_transfer_encoding`` parameter in :py:meth:`FormData.add_field() | ||
| 50 | +<aiohttp.FormData.add_field>` -- by :user:`Dreamsorcerer`. | ||
| 51 | diff --git a/CHANGES/8332.bugfix.rst b/CHANGES/8332.bugfix.rst | ||
| 52 | new file mode 100644 | ||
| 53 | index 0000000..70cad26 | ||
| 54 | --- /dev/null | ||
| 55 | +++ b/CHANGES/8332.bugfix.rst | ||
| 56 | @@ -0,0 +1 @@ | ||
| 57 | +Fixed regression with adding Content-Disposition to form-data part after appending to writer -- by :user:`Dreamsorcerer`/:user:`Olegt0rr`. | ||
| 58 | diff --git a/CHANGES/8335.bugfix.rst b/CHANGES/8335.bugfix.rst | ||
| 59 | new file mode 100644 | ||
| 60 | index 0000000..cd93b86 | ||
| 61 | --- /dev/null | ||
| 62 | +++ b/CHANGES/8335.bugfix.rst | ||
| 63 | @@ -0,0 +1 @@ | ||
| 64 | +Added default Content-Disposition in multipart/form-data responses -- by :user:`Dreamsorcerer`. | ||
| 65 | diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py | ||
| 66 | index e7cd24c..2b75b3d 100644 | ||
| 67 | --- a/aiohttp/formdata.py | ||
| 68 | +++ b/aiohttp/formdata.py | ||
| 69 | @@ -1,4 +1,5 @@ | ||
| 70 | import io | ||
| 71 | +import warnings | ||
| 72 | from typing import Any, Iterable, List, Optional | ||
| 73 | from urllib.parse import urlencode | ||
| 74 | |||
| 75 | @@ -53,7 +54,12 @@ class FormData: | ||
| 76 | if isinstance(value, io.IOBase): | ||
| 77 | self._is_multipart = True | ||
| 78 | elif isinstance(value, (bytes, bytearray, memoryview)): | ||
| 79 | + msg = ( | ||
| 80 | + "In v4, passing bytes will no longer create a file field. " | ||
| 81 | + "Please explicitly use the filename parameter or pass a BytesIO object." | ||
| 82 | + ) | ||
| 83 | if filename is None and content_transfer_encoding is None: | ||
| 84 | + warnings.warn(msg, DeprecationWarning) | ||
| 85 | filename = name | ||
| 86 | |||
| 87 | type_options: MultiDict[str] = MultiDict({"name": name}) | ||
| 88 | @@ -81,7 +87,11 @@ class FormData: | ||
| 89 | "content_transfer_encoding must be an instance" | ||
| 90 | " of str. Got: %s" % content_transfer_encoding | ||
| 91 | ) | ||
| 92 | - headers[hdrs.CONTENT_TRANSFER_ENCODING] = content_transfer_encoding | ||
| 93 | + msg = ( | ||
| 94 | + "content_transfer_encoding is deprecated. " | ||
| 95 | + "To maintain compatibility with v4 please pass a BytesPayload." | ||
| 96 | + ) | ||
| 97 | + warnings.warn(msg, DeprecationWarning) | ||
| 98 | self._is_multipart = True | ||
| 99 | |||
| 100 | self._fields.append((type_options, headers, value)) | ||
| 101 | diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py | ||
| 102 | index 73801f4..9cd49bb 100644 | ||
| 103 | --- a/aiohttp/multipart.py | ||
| 104 | +++ b/aiohttp/multipart.py | ||
| 105 | @@ -255,13 +255,22 @@ class BodyPartReader: | ||
| 106 | chunk_size = 8192 | ||
| 107 | |||
| 108 | def __init__( | ||
| 109 | - self, boundary: bytes, headers: "CIMultiDictProxy[str]", content: StreamReader | ||
| 110 | + self, | ||
| 111 | + boundary: bytes, | ||
| 112 | + headers: "CIMultiDictProxy[str]", | ||
| 113 | + content: StreamReader, | ||
| 114 | + *, | ||
| 115 | + subtype: str = "mixed", | ||
| 116 | + default_charset: Optional[str] = None, | ||
| 117 | ) -> None: | ||
| 118 | self.headers = headers | ||
| 119 | self._boundary = boundary | ||
| 120 | self._content = content | ||
| 121 | + self._default_charset = default_charset | ||
| 122 | self._at_eof = False | ||
| 123 | - length = self.headers.get(CONTENT_LENGTH, None) | ||
| 124 | + self._is_form_data = subtype == "form-data" | ||
| 125 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 126 | + length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) | ||
| 127 | self._length = int(length) if length is not None else None | ||
| 128 | self._read_bytes = 0 | ||
| 129 | # TODO: typeing.Deque is not supported by Python 3.5 | ||
| 130 | @@ -329,6 +338,8 @@ class BodyPartReader: | ||
| 131 | assert self._length is not None, "Content-Length required for chunked read" | ||
| 132 | chunk_size = min(size, self._length - self._read_bytes) | ||
| 133 | chunk = await self._content.read(chunk_size) | ||
| 134 | + if self._content.at_eof(): | ||
| 135 | + self._at_eof = True | ||
| 136 | return chunk | ||
| 137 | |||
| 138 | async def _read_chunk_from_stream(self, size: int) -> bytes: | ||
| 139 | @@ -444,7 +455,8 @@ class BodyPartReader: | ||
| 140 | """ | ||
| 141 | if CONTENT_TRANSFER_ENCODING in self.headers: | ||
| 142 | data = self._decode_content_transfer(data) | ||
| 143 | - if CONTENT_ENCODING in self.headers: | ||
| 144 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 145 | + if not self._is_form_data and CONTENT_ENCODING in self.headers: | ||
| 146 | return self._decode_content(data) | ||
| 147 | return data | ||
| 148 | |||
| 149 | @@ -478,7 +490,7 @@ class BodyPartReader: | ||
| 150 | """Returns charset parameter from Content-Type header or default.""" | ||
| 151 | ctype = self.headers.get(CONTENT_TYPE, "") | ||
| 152 | mimetype = parse_mimetype(ctype) | ||
| 153 | - return mimetype.parameters.get("charset", default) | ||
| 154 | + return mimetype.parameters.get("charset", self._default_charset or default) | ||
| 155 | |||
| 156 | @reify | ||
| 157 | def name(self) -> Optional[str]: | ||
| 158 | @@ -533,9 +545,17 @@ class MultipartReader: | ||
| 159 | part_reader_cls = BodyPartReader | ||
| 160 | |||
| 161 | def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None: | ||
| 162 | + self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) | ||
| 163 | + assert self._mimetype.type == "multipart", "multipart/* content type expected" | ||
| 164 | + if "boundary" not in self._mimetype.parameters: | ||
| 165 | + raise ValueError( | ||
| 166 | + "boundary missed for Content-Type: %s" % headers[CONTENT_TYPE] | ||
| 167 | + ) | ||
| 168 | + | ||
| 169 | self.headers = headers | ||
| 170 | self._boundary = ("--" + self._get_boundary()).encode() | ||
| 171 | self._content = content | ||
| 172 | + self._default_charset: Optional[str] = None | ||
| 173 | self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None | ||
| 174 | self._at_eof = False | ||
| 175 | self._at_bof = True | ||
| 176 | @@ -587,7 +607,24 @@ class MultipartReader: | ||
| 177 | await self._read_boundary() | ||
| 178 | if self._at_eof: # we just read the last boundary, nothing to do there | ||
| 179 | return None | ||
| 180 | - self._last_part = await self.fetch_next_part() | ||
| 181 | + | ||
| 182 | + part = await self.fetch_next_part() | ||
| 183 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.6 | ||
| 184 | + if ( | ||
| 185 | + self._last_part is None | ||
| 186 | + and self._mimetype.subtype == "form-data" | ||
| 187 | + and isinstance(part, BodyPartReader) | ||
| 188 | + ): | ||
| 189 | + _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION)) | ||
| 190 | + if params.get("name") == "_charset_": | ||
| 191 | + # Longest encoding in https://encoding.spec.whatwg.org/encodings.json | ||
| 192 | + # is 19 characters, so 32 should be more than enough for any valid encoding. | ||
| 193 | + charset = await part.read_chunk(32) | ||
| 194 | + if len(charset) > 31: | ||
| 195 | + raise RuntimeError("Invalid default charset") | ||
| 196 | + self._default_charset = charset.strip().decode() | ||
| 197 | + part = await self.fetch_next_part() | ||
| 198 | + self._last_part = part | ||
| 199 | return self._last_part | ||
| 200 | |||
| 201 | async def release(self) -> None: | ||
| 202 | @@ -623,19 +660,16 @@ class MultipartReader: | ||
| 203 | return type(self)(headers, self._content) | ||
| 204 | return self.multipart_reader_cls(headers, self._content) | ||
| 205 | else: | ||
| 206 | - return self.part_reader_cls(self._boundary, headers, self._content) | ||
| 207 | - | ||
| 208 | - def _get_boundary(self) -> str: | ||
| 209 | - mimetype = parse_mimetype(self.headers[CONTENT_TYPE]) | ||
| 210 | - | ||
| 211 | - assert mimetype.type == "multipart", "multipart/* content type expected" | ||
| 212 | - | ||
| 213 | - if "boundary" not in mimetype.parameters: | ||
| 214 | - raise ValueError( | ||
| 215 | - "boundary missed for Content-Type: %s" % self.headers[CONTENT_TYPE] | ||
| 216 | + return self.part_reader_cls( | ||
| 217 | + self._boundary, | ||
| 218 | + headers, | ||
| 219 | + self._content, | ||
| 220 | + subtype=self._mimetype.subtype, | ||
| 221 | + default_charset=self._default_charset, | ||
| 222 | ) | ||
| 223 | |||
| 224 | - boundary = mimetype.parameters["boundary"] | ||
| 225 | + def _get_boundary(self) -> str: | ||
| 226 | + boundary = self._mimetype.parameters["boundary"] | ||
| 227 | if len(boundary) > 70: | ||
| 228 | raise ValueError("boundary %r is too long (70 chars max)" % boundary) | ||
| 229 | |||
| 230 | @@ -726,6 +760,7 @@ class MultipartWriter(Payload): | ||
| 231 | super().__init__(None, content_type=ctype) | ||
| 232 | |||
| 233 | self._parts: List[_Part] = [] | ||
| 234 | + self._is_form_data = subtype == "form-data" | ||
| 235 | |||
| 236 | def __enter__(self) -> "MultipartWriter": | ||
| 237 | return self | ||
| 238 | @@ -803,32 +838,38 @@ class MultipartWriter(Payload): | ||
| 239 | |||
| 240 | def append_payload(self, payload: Payload) -> Payload: | ||
| 241 | """Adds a new body part to multipart writer.""" | ||
| 242 | - # compression | ||
| 243 | - encoding: Optional[str] = payload.headers.get( | ||
| 244 | - CONTENT_ENCODING, | ||
| 245 | - "", | ||
| 246 | - ).lower() | ||
| 247 | - if encoding and encoding not in ("deflate", "gzip", "identity"): | ||
| 248 | - raise RuntimeError(f"unknown content encoding: {encoding}") | ||
| 249 | - if encoding == "identity": | ||
| 250 | - encoding = None | ||
| 251 | - | ||
| 252 | - # te encoding | ||
| 253 | - te_encoding: Optional[str] = payload.headers.get( | ||
| 254 | - CONTENT_TRANSFER_ENCODING, | ||
| 255 | - "", | ||
| 256 | - ).lower() | ||
| 257 | - if te_encoding not in ("", "base64", "quoted-printable", "binary"): | ||
| 258 | - raise RuntimeError( | ||
| 259 | - "unknown content transfer encoding: {}" "".format(te_encoding) | ||
| 260 | + encoding: Optional[str] = None | ||
| 261 | + te_encoding: Optional[str] = None | ||
| 262 | + if self._is_form_data: | ||
| 263 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7 | ||
| 264 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 | ||
| 265 | + assert ( | ||
| 266 | + not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} | ||
| 267 | + & payload.headers.keys() | ||
| 268 | ) | ||
| 269 | - if te_encoding == "binary": | ||
| 270 | - te_encoding = None | ||
| 271 | - | ||
| 272 | - # size | ||
| 273 | - size = payload.size | ||
| 274 | - if size is not None and not (encoding or te_encoding): | ||
| 275 | - payload.headers[CONTENT_LENGTH] = str(size) | ||
| 276 | + # Set default Content-Disposition in case user doesn't create one | ||
| 277 | + if CONTENT_DISPOSITION not in payload.headers: | ||
| 278 | + name = f"section-{len(self._parts)}" | ||
| 279 | + payload.set_content_disposition("form-data", name=name) | ||
| 280 | + else: | ||
| 281 | + # compression | ||
| 282 | + encoding = payload.headers.get(CONTENT_ENCODING, "").lower() | ||
| 283 | + if encoding and encoding not in ("deflate", "gzip", "identity"): | ||
| 284 | + raise RuntimeError(f"unknown content encoding: {encoding}") | ||
| 285 | + if encoding == "identity": | ||
| 286 | + encoding = None | ||
| 287 | + | ||
| 288 | + # te encoding | ||
| 289 | + te_encoding = payload.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() | ||
| 290 | + if te_encoding not in ("", "base64", "quoted-printable", "binary"): | ||
| 291 | + raise RuntimeError(f"unknown content transfer encoding: {te_encoding}") | ||
| 292 | + if te_encoding == "binary": | ||
| 293 | + te_encoding = None | ||
| 294 | + | ||
| 295 | + # size | ||
| 296 | + size = payload.size | ||
| 297 | + if size is not None and not (encoding or te_encoding): | ||
| 298 | + payload.headers[CONTENT_LENGTH] = str(size) | ||
| 299 | |||
| 300 | self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type] | ||
| 301 | return payload | ||
| 302 | @@ -886,6 +927,11 @@ class MultipartWriter(Payload): | ||
| 303 | async def write(self, writer: Any, close_boundary: bool = True) -> None: | ||
| 304 | """Write body.""" | ||
| 305 | for part, encoding, te_encoding in self._parts: | ||
| 306 | + if self._is_form_data: | ||
| 307 | + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 | ||
| 308 | + assert CONTENT_DISPOSITION in part.headers | ||
| 309 | + assert "name=" in part.headers[CONTENT_DISPOSITION] | ||
| 310 | + | ||
| 311 | await writer.write(b"--" + self._boundary + b"\r\n") | ||
| 312 | await writer.write(part._binary_headers) | ||
| 313 | |||
| 314 | diff --git a/tests/test_multipart.py b/tests/test_multipart.py | ||
| 315 | index cc3f5ff..1d036fb 100644 | ||
| 316 | --- a/tests/test_multipart.py | ||
| 317 | +++ b/tests/test_multipart.py | ||
| 318 | @@ -942,6 +942,58 @@ class TestMultipartReader: | ||
| 319 | assert first.at_eof() | ||
| 320 | assert not second.at_eof() | ||
| 321 | |||
| 322 | + async def test_read_form_default_encoding(self) -> None: | ||
| 323 | + with Stream( | ||
| 324 | + b"--:\r\n" | ||
| 325 | + b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' | ||
| 326 | + b"ascii" | ||
| 327 | + b"\r\n" | ||
| 328 | + b"--:\r\n" | ||
| 329 | + b'Content-Disposition: form-data; name="field1"\r\n\r\n' | ||
| 330 | + b"foo" | ||
| 331 | + b"\r\n" | ||
| 332 | + b"--:\r\n" | ||
| 333 | + b"Content-Type: text/plain;charset=UTF-8\r\n" | ||
| 334 | + b'Content-Disposition: form-data; name="field2"\r\n\r\n' | ||
| 335 | + b"foo" | ||
| 336 | + b"\r\n" | ||
| 337 | + b"--:\r\n" | ||
| 338 | + b'Content-Disposition: form-data; name="field3"\r\n\r\n' | ||
| 339 | + b"foo" | ||
| 340 | + b"\r\n" | ||
| 341 | + ) as stream: | ||
| 342 | + reader = aiohttp.MultipartReader( | ||
| 343 | + {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, | ||
| 344 | + stream, | ||
| 345 | + ) | ||
| 346 | + field1 = await reader.next() | ||
| 347 | + assert field1.name == "field1" | ||
| 348 | + assert field1.get_charset("default") == "ascii" | ||
| 349 | + field2 = await reader.next() | ||
| 350 | + assert field2.name == "field2" | ||
| 351 | + assert field2.get_charset("default") == "UTF-8" | ||
| 352 | + field3 = await reader.next() | ||
| 353 | + assert field3.name == "field3" | ||
| 354 | + assert field3.get_charset("default") == "ascii" | ||
| 355 | + | ||
| 356 | + async def test_read_form_invalid_default_encoding(self) -> None: | ||
| 357 | + with Stream( | ||
| 358 | + b"--:\r\n" | ||
| 359 | + b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' | ||
| 360 | + b"this-value-is-too-long-to-be-a-charset" | ||
| 361 | + b"\r\n" | ||
| 362 | + b"--:\r\n" | ||
| 363 | + b'Content-Disposition: form-data; name="field1"\r\n\r\n' | ||
| 364 | + b"foo" | ||
| 365 | + b"\r\n" | ||
| 366 | + ) as stream: | ||
| 367 | + reader = aiohttp.MultipartReader( | ||
| 368 | + {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, | ||
| 369 | + stream, | ||
| 370 | + ) | ||
| 371 | + with pytest.raises(RuntimeError, match="Invalid default charset"): | ||
| 372 | + await reader.next() | ||
| 373 | + | ||
| 374 | |||
| 375 | async def test_writer(writer) -> None: | ||
| 376 | assert writer.size == 7 | ||
| 377 | @@ -1228,6 +1280,25 @@ class TestMultipartWriter: | ||
| 378 | part = writer._parts[0][0] | ||
| 379 | assert part.headers[CONTENT_TYPE] == "test/passed" | ||
| 380 | |||
| 381 | + def test_set_content_disposition_after_append(self): | ||
| 382 | + writer = aiohttp.MultipartWriter("form-data") | ||
| 383 | + part = writer.append("some-data") | ||
| 384 | + part.set_content_disposition("form-data", name="method") | ||
| 385 | + assert 'name="method"' in part.headers[CONTENT_DISPOSITION] | ||
| 386 | + | ||
| 387 | + def test_automatic_content_disposition(self): | ||
| 388 | + writer = aiohttp.MultipartWriter("form-data") | ||
| 389 | + writer.append_json(()) | ||
| 390 | + part = payload.StringPayload("foo") | ||
| 391 | + part.set_content_disposition("form-data", name="second") | ||
| 392 | + writer.append_payload(part) | ||
| 393 | + writer.append("foo") | ||
| 394 | + | ||
| 395 | + disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts) | ||
| 396 | + assert 'name="section-0"' in disps[0] | ||
| 397 | + assert 'name="second"' in disps[1] | ||
| 398 | + assert 'name="section-2"' in disps[2] | ||
| 399 | + | ||
| 400 | def test_with(self) -> None: | ||
| 401 | with aiohttp.MultipartWriter(boundary=":") as writer: | ||
| 402 | writer.append("foo") | ||
| 403 | @@ -1278,7 +1349,6 @@ class TestMultipartWriter: | ||
| 404 | CONTENT_TYPE: "text/python", | ||
| 405 | }, | ||
| 406 | ) | ||
| 407 | - content_length = part.size | ||
| 408 | await writer.write(stream) | ||
| 409 | |||
| 410 | assert part.headers[CONTENT_TYPE] == "text/python" | ||
| 411 | @@ -1289,9 +1359,7 @@ class TestMultipartWriter: | ||
| 412 | assert headers == ( | ||
| 413 | b"--:\r\n" | ||
| 414 | b"Content-Type: text/python\r\n" | ||
| 415 | - b'Content-Disposition: attachments; filename="bug.py"\r\n' | ||
| 416 | - b"Content-Length: %s" | ||
| 417 | - b"" % (str(content_length).encode(),) | ||
| 418 | + b'Content-Disposition: attachments; filename="bug.py"' | ||
| 419 | ) | ||
| 420 | |||
| 421 | async def test_set_content_disposition_override(self, buf, stream): | ||
| 422 | @@ -1305,7 +1373,6 @@ class TestMultipartWriter: | ||
| 423 | CONTENT_TYPE: "text/python", | ||
| 424 | }, | ||
| 425 | ) | ||
| 426 | - content_length = part.size | ||
| 427 | await writer.write(stream) | ||
| 428 | |||
| 429 | assert part.headers[CONTENT_TYPE] == "text/python" | ||
| 430 | @@ -1316,9 +1383,7 @@ class TestMultipartWriter: | ||
| 431 | assert headers == ( | ||
| 432 | b"--:\r\n" | ||
| 433 | b"Content-Type: text/python\r\n" | ||
| 434 | - b'Content-Disposition: attachments; filename="bug.py"\r\n' | ||
| 435 | - b"Content-Length: %s" | ||
| 436 | - b"" % (str(content_length).encode(),) | ||
| 437 | + b'Content-Disposition: attachments; filename="bug.py"' | ||
| 438 | ) | ||
| 439 | |||
| 440 | async def test_reset_content_disposition_header(self, buf, stream): | ||
| 441 | @@ -1330,8 +1395,6 @@ class TestMultipartWriter: | ||
| 442 | headers={CONTENT_TYPE: "text/plain"}, | ||
| 443 | ) | ||
| 444 | |||
| 445 | - content_length = part.size | ||
| 446 | - | ||
| 447 | assert CONTENT_DISPOSITION in part.headers | ||
| 448 | |||
| 449 | part.set_content_disposition("attachments", filename="bug.py") | ||
| 450 | @@ -1344,9 +1407,7 @@ class TestMultipartWriter: | ||
| 451 | b"--:\r\n" | ||
| 452 | b"Content-Type: text/plain\r\n" | ||
| 453 | b"Content-Disposition:" | ||
| 454 | - b' attachments; filename="bug.py"\r\n' | ||
| 455 | - b"Content-Length: %s" | ||
| 456 | - b"" % (str(content_length).encode(),) | ||
| 457 | + b' attachments; filename="bug.py"' | ||
| 458 | ) | ||
| 459 | |||
| 460 | |||
| 461 | diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py | ||
| 462 | index 5fdfb23..61739e9 100644 | ||
| 463 | --- a/tests/test_web_functional.py | ||
| 464 | +++ b/tests/test_web_functional.py | ||
| 465 | @@ -34,7 +34,8 @@ def fname(here): | ||
| 466 | |||
| 467 | def new_dummy_form(): | ||
| 468 | form = FormData() | ||
| 469 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 470 | + with pytest.warns(DeprecationWarning, match="BytesPayload"): | ||
| 471 | + form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 472 | return form | ||
| 473 | |||
| 474 | |||
| 475 | @@ -429,25 +430,6 @@ async def test_release_post_data(aiohttp_client) -> None: | ||
| 476 | await resp.release() | ||
| 477 | |||
| 478 | |||
| 479 | -async def test_POST_DATA_with_content_transfer_encoding(aiohttp_client) -> None: | ||
| 480 | - async def handler(request): | ||
| 481 | - data = await request.post() | ||
| 482 | - assert b"123" == data["name"] | ||
| 483 | - return web.Response() | ||
| 484 | - | ||
| 485 | - app = web.Application() | ||
| 486 | - app.router.add_post("/", handler) | ||
| 487 | - client = await aiohttp_client(app) | ||
| 488 | - | ||
| 489 | - form = FormData() | ||
| 490 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 491 | - | ||
| 492 | - resp = await client.post("/", data=form) | ||
| 493 | - assert 200 == resp.status | ||
| 494 | - | ||
| 495 | - await resp.release() | ||
| 496 | - | ||
| 497 | - | ||
| 498 | async def test_post_form_with_duplicate_keys(aiohttp_client) -> None: | ||
| 499 | async def handler(request): | ||
| 500 | data = await request.post() | ||
| 501 | @@ -505,7 +487,8 @@ async def test_100_continue(aiohttp_client) -> None: | ||
| 502 | return web.Response() | ||
| 503 | |||
| 504 | form = FormData() | ||
| 505 | - form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 506 | + with pytest.warns(DeprecationWarning, match="BytesPayload"): | ||
| 507 | + form.add_field("name", b"123", content_transfer_encoding="base64") | ||
| 508 | |||
| 509 | app = web.Application() | ||
| 510 | app.router.add_post("/", handler) | ||
| 511 | @@ -683,7 +666,7 @@ async def test_upload_file(aiohttp_client) -> None: | ||
| 512 | app.router.add_post("/", handler) | ||
| 513 | client = await aiohttp_client(app) | ||
| 514 | |||
| 515 | - resp = await client.post("/", data={"file": data}) | ||
| 516 | + resp = await client.post("/", data={"file": io.BytesIO(data)}) | ||
| 517 | assert 200 == resp.status | ||
| 518 | |||
| 519 | await resp.release() | ||
| 520 | -- | ||
| 521 | 2.25.1 | ||
| 522 | |||
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-52304.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-52304.patch new file mode 100644 index 0000000000..a76968c6ca --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-52304.patch | |||
| @@ -0,0 +1,46 @@ | |||
| 1 | From 27b9925ad3ac716a6db3a3d1214b3fe2a260c5c8 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: "J. Nick Koston" <nick@koston.org> | ||
| 3 | Date: Wed, 13 Nov 2024 08:50:36 -0600 | ||
| 4 | Subject: [PATCH] Fix incorrect parsing of chunk extensions with the pure | ||
| 5 | Python parser (#9853) | ||
| 6 | |||
| 7 | Upstream-Status: Backport | ||
| 8 | [https://github.com/aio-libs/aiohttp/commit/259edc369075de63e6f3a4eaade058c62af0df71] | ||
| 9 | |||
| 10 | CVE: CVE-2024-52304 | ||
| 11 | |||
| 12 | Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com> | ||
| 13 | --- | ||
| 14 | CHANGES/9851.bugfix.rst | 1 + | ||
| 15 | aiohttp/http_parser.py | 7 +++++++ | ||
| 16 | 2 files changed, 8 insertions(+) | ||
| 17 | create mode 100644 CHANGES/9851.bugfix.rst | ||
| 18 | |||
| 19 | diff --git a/CHANGES/9851.bugfix.rst b/CHANGES/9851.bugfix.rst | ||
| 20 | new file mode 100644 | ||
| 21 | index 0000000..02541a9 | ||
| 22 | --- /dev/null | ||
| 23 | +++ b/CHANGES/9851.bugfix.rst | ||
| 24 | @@ -0,0 +1 @@ | ||
| 25 | +Fixed incorrect parsing of chunk extensions with the pure Python parser -- by :user:`bdraco`. | ||
| 26 | diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py | ||
| 27 | index 91e01f4..1ee1269 100644 | ||
| 28 | --- a/aiohttp/http_parser.py | ||
| 29 | +++ b/aiohttp/http_parser.py | ||
| 30 | @@ -820,6 +820,13 @@ class HttpPayloadParser: | ||
| 31 | i = chunk.find(CHUNK_EXT, 0, pos) | ||
| 32 | if i >= 0: | ||
| 33 | size_b = chunk[:i] # strip chunk-extensions | ||
| 34 | + # Verify no LF in the chunk-extension | ||
| 35 | + if b"\n" in (ext := chunk[i:pos]): | ||
| 36 | + exc = BadHttpMessage( | ||
| 37 | + f"Unexpected LF in chunk-extension: {ext!r}" | ||
| 38 | + ) | ||
| 39 | + set_exception(self.payload, exc) | ||
| 40 | + raise exc | ||
| 41 | else: | ||
| 42 | size_b = chunk[:pos] | ||
| 43 | |||
| 44 | -- | ||
| 45 | 2.25.1 | ||
| 46 | |||
