diff --git a/CVE-2024-27306.patch b/CVE-2024-27306.patch deleted file mode 100644 index 6449b2c25ef202b64ea73840d47a8792e0984173..0000000000000000000000000000000000000000 --- a/CVE-2024-27306.patch +++ /dev/null @@ -1,242 +0,0 @@ -From 28335525d1eac015a7e7584137678cbb6ff19397 Mon Sep 17 00:00:00 2001 -From: Sam Bull -Date: Thu, 11 Apr 2024 15:54:45 +0100 -Subject: [PATCH] Escape filenames and paths in HTML when generating index - pages (#8317) (#8319) - -Co-authored-by: J. Nick Koston -(cherry picked from commit ffbc43233209df302863712b511a11bdb6001b0f) ---- - CHANGES/8317.bugfix.rst | 1 + - aiohttp/web_urldispatcher.py | 12 ++-- - tests/test_web_urldispatcher.py | 124 ++++++++++++++++++++++++++++---- - 3 files changed, 118 insertions(+), 19 deletions(-) - create mode 100644 CHANGES/8317.bugfix.rst - -diff --git a/CHANGES/8317.bugfix.rst b/CHANGES/8317.bugfix.rst -new file mode 100644 -index 0000000000..b24ef2aeb8 ---- /dev/null -+++ b/CHANGES/8317.bugfix.rst -@@ -0,0 +1 @@ -+Escaped filenames in static view -- by :user:`bdraco`. -diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py -index 9969653344..954291f644 100644 ---- a/aiohttp/web_urldispatcher.py -+++ b/aiohttp/web_urldispatcher.py -@@ -1,7 +1,9 @@ - import abc - import asyncio - import base64 -+import functools - import hashlib -+import html - import inspect - import keyword - import os -@@ -90,6 +92,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 -@@ -708,7 +712,7 @@ 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"

{index_of}

" - - index_list = [] -@@ -716,7 +720,7 @@ def _directory_as_html(self, filepath: Path) -> str: - 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(): -@@ -725,9 +729,7 @@ def _directory_as_html(self, filepath: Path) -> str: - file_name = _file.name - - index_list.append( -- '
  • {name}
  • '.format( -- url=file_url, name=file_name -- ) -+ f'
  • {html_escape(file_name)}
  • ' - ) - ul = "".format("\n".join(index_list)) - body = f"\n{h1}\n{ul}\n" -diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py -index 76e533e473..0441890c10 100644 ---- a/tests/test_web_urldispatcher.py -+++ b/tests/test_web_urldispatcher.py -@@ -1,6 +1,7 @@ - import asyncio - import functools - import pathlib -+import sys - from typing import Optional - from unittest import mock - from unittest.mock import MagicMock -@@ -14,31 +15,38 @@ - - - @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"\n\nIndex of /.\n" -- b"\n\n

    Index of /.

    \n\n\n", -- id="index_root", -+ "/", -+ b"\n\nIndex of /.\n\n\n

    Index of" -+ b' /.

    \n\n\n", - ), - pytest.param( - True, - 200, - "/static", -- b"\n\nIndex of /.\n" -- b"\n\n

    Index of /.

    \n\n\n", -+ "/static", -+ b"\n\nIndex of /.\n\n\n

    Index of" -+ b' /.

    \n\n\n', - id="index_static", - ), -+ pytest.param( -+ True, -+ 200, -+ "/static", -+ "/static/my_dir", -+ b"\n\nIndex of /my_dir\n\n\n

    " -+ b'Index of /my_dir

    \n\n\n", -+ id="index_subdir", -+ ), - ], - ) - async def test_access_root_of_static_handler( -@@ -47,6 +55,7 @@ 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. -@@ -72,7 +81,94 @@ 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: -+ assert r.headers["Content-Type"] == "text/html; charset=utf-8" -+ read_ = await r.read() -+ 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"\n\nIndex of /.\n\n\n

    Index of" -+ b' /.

    \n\n\n", -+ ), -+ pytest.param( -+ True, -+ 200, -+ "/static", -+ "/static", -+ b"\n\nIndex of /.\n\n\n

    Index of" -+ b' /.

    \n\n\n", -+ id="index_static", -+ ), -+ pytest.param( -+ True, -+ 200, -+ "/static", -+ "/static/.dir", -+ b"\n\nIndex 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: pathlib.Path, -+ aiohttp_client: AiohttpClient, -+ 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. -+ # 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: diff --git a/CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch b/CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch deleted file mode 100644 index 2dcce4d4a0988fd9a7c5a859efcc9dea352bb32e..0000000000000000000000000000000000000000 --- a/CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch +++ /dev/null @@ -1,67 +0,0 @@ -From 7eecdff163ccf029fbb1ddc9de4169d4aaeb6597 Mon Sep 17 00:00:00 2001 -From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> -Date: Mon, 15 Apr 2024 20:47:19 +0100 -Subject: [PATCH] [PR #8332/482e6cdf backport][3.9] Add set_content_disposition - test (#8333) - -**This is a backport of PR #8332 as merged into master -(482e6cdf6516607360666a48c5828d3dbe959fbd).** - -Co-authored-by: Oleg A <t0rr@mail.ru> ---- - CHANGES/8332.bugfix.rst | 1 + - aiohttp/multipart.py | 7 +++++-- - tests/test_multipart.py | 7 +++++++ - 3 files changed, 13 insertions(+), 2 deletions(-) - create mode 100644 CHANGES/8332.bugfix.rst - -diff --git a/CHANGES/8332.bugfix.rst b/CHANGES/8332.bugfix.rst -new file mode 100644 -index 0000000000..70cad26b42 ---- /dev/null -+++ b/CHANGES/8332.bugfix.rst -@@ -0,0 +1 @@ -+Fixed regression with adding Content-Disposition to form-data part after appending to writer -- by :user:`Dreamsorcerer`/:user:`Olegt0rr`. -diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py -index a43ec54571..fcdf16183c 100644 ---- a/aiohttp/multipart.py -+++ b/aiohttp/multipart.py -@@ -848,8 +848,6 @@ def append_payload(self, payload: Payload) -> Payload: - if self._is_form_data: - # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7 - # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 -- assert CONTENT_DISPOSITION in payload.headers -- assert "name=" in payload.headers[CONTENT_DISPOSITION] - assert ( - not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} - & payload.headers.keys() -@@ -930,6 +928,11 @@ def size(self) -> Optional[int]: - async def write(self, writer: Any, close_boundary: bool = True) -> None: - """Write body.""" - for part, encoding, te_encoding in self._parts: -+ if self._is_form_data: -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 -+ assert CONTENT_DISPOSITION in part.headers -+ assert "name=" in part.headers[CONTENT_DISPOSITION] -+ - await writer.write(b"--" + self._boundary + b"\r\n") - await writer.write(part._binary_headers) - -diff --git a/tests/test_multipart.py b/tests/test_multipart.py -index dbfaf74b9b..37ac54797f 100644 ---- a/tests/test_multipart.py -+++ b/tests/test_multipart.py -@@ -1282,6 +1282,13 @@ def test_append_multipart(self, writer) -> None: - part = writer._parts[0][0] - assert part.headers[CONTENT_TYPE] == "test/passed" - -+ async def test_set_content_disposition_after_append(self): -+ writer = aiohttp.MultipartWriter("form-data") -+ payload = writer.append("some-data") -+ payload.set_content_disposition("form-data", name="method") -+ assert CONTENT_DISPOSITION in payload.headers -+ assert "name=" in payload.headers[CONTENT_DISPOSITION] -+ - def test_with(self) -> None: - with aiohttp.MultipartWriter(boundary=":") as writer: - writer.append("foo") diff --git a/CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch b/CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch deleted file mode 100644 index f302e2a0b146aee9fc93a276874aeda793669fe6..0000000000000000000000000000000000000000 --- a/CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch +++ /dev/null @@ -1,73 +0,0 @@ -From f21c6f2ca512a026ce7f0f6c6311f62d6a638866 Mon Sep 17 00:00:00 2001 -From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> -Date: Mon, 15 Apr 2024 21:54:12 +0100 -Subject: [PATCH] [PR #8335/5a6949da backport][3.9] Add Content-Disposition - automatically (#8336) - -**This is a backport of PR #8335 as merged into master -(5a6949da642d1db6cf414fd0d1f70e54c7b7be14).** - -Co-authored-by: Sam Bull <git@sambull.org> ---- - CHANGES/8335.bugfix.rst | 1 + - aiohttp/multipart.py | 4 ++++ - tests/test_multipart.py | 22 +++++++++++++++++----- - 3 files changed, 22 insertions(+), 5 deletions(-) - create mode 100644 CHANGES/8335.bugfix.rst - -diff --git a/CHANGES/8335.bugfix.rst b/CHANGES/8335.bugfix.rst -new file mode 100644 -index 0000000000..cd93b864a5 ---- /dev/null -+++ b/CHANGES/8335.bugfix.rst -@@ -0,0 +1 @@ -+Added default Content-Disposition in multipart/form-data responses -- by :user:`Dreamsorcerer`. -diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py -index fcdf16183c..71fc2654a1 100644 ---- a/aiohttp/multipart.py -+++ b/aiohttp/multipart.py -@@ -852,6 +852,10 @@ def append_payload(self, payload: Payload) -> Payload: - not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} - & payload.headers.keys() - ) -+ # Set default Content-Disposition in case user doesn't create one -+ if CONTENT_DISPOSITION not in payload.headers: -+ name = f"section-{len(self._parts)}" -+ payload.set_content_disposition("form-data", name=name) - else: - # compression - encoding = payload.headers.get(CONTENT_ENCODING, "").lower() -diff --git a/tests/test_multipart.py b/tests/test_multipart.py -index 37ac54797f..436b70957f 100644 ---- a/tests/test_multipart.py -+++ b/tests/test_multipart.py -@@ -1282,12 +1282,24 @@ def test_append_multipart(self, writer) -> None: - part = writer._parts[0][0] - assert part.headers[CONTENT_TYPE] == "test/passed" - -- async def test_set_content_disposition_after_append(self): -+ def test_set_content_disposition_after_append(self): - writer = aiohttp.MultipartWriter("form-data") -- payload = writer.append("some-data") -- payload.set_content_disposition("form-data", name="method") -- assert CONTENT_DISPOSITION in payload.headers -- assert "name=" in payload.headers[CONTENT_DISPOSITION] -+ part = writer.append("some-data") -+ part.set_content_disposition("form-data", name="method") -+ assert 'name="method"' in part.headers[CONTENT_DISPOSITION] -+ -+ def test_automatic_content_disposition(self): -+ writer = aiohttp.MultipartWriter("form-data") -+ writer.append_json(()) -+ part = payload.StringPayload("foo") -+ part.set_content_disposition("form-data", name="second") -+ writer.append_payload(part) -+ writer.append("foo") -+ -+ disps = tuple(p[0].headers[CONTENT_DISPOSITION] for p in writer._parts) -+ assert 'name="section-0"' in disps[0] -+ assert 'name="second"' in disps[1] -+ assert 'name="section-2"' in disps[2] - - def test_with(self) -> None: - with aiohttp.MultipartWriter(boundary=":") as writer: diff --git a/CVE-2024-30251.patch b/CVE-2024-30251.patch deleted file mode 100644 index 828022dbfe4e5cfd67881d83f5f2e16def8c27fd..0000000000000000000000000000000000000000 --- a/CVE-2024-30251.patch +++ /dev/null @@ -1,511 +0,0 @@ -From cebe526b9c34dc3a3da9140409db63014bc4cf19 Mon Sep 17 00:00:00 2001 -From: Sam Bull <git@sambull.org> -Date: Sun, 7 Apr 2024 13:19:31 +0100 -Subject: [PATCH] Fix handling of multipart/form-data (#8280) (#8302) - -https://datatracker.ietf.org/doc/html/rfc7578 -(cherry picked from commit 7d0be3fee540a3d4161ac7dc76422f1f5ea60104) ---- - CHANGES/8280.bugfix.rst | 1 + - CHANGES/8280.deprecation.rst | 2 + - aiohttp/formdata.py | 12 +++- - aiohttp/multipart.py | 121 +++++++++++++++++++++----------- - tests/test_client_functional.py | 44 +----------- - tests/test_multipart.py | 68 ++++++++++++++---- - tests/test_web_functional.py | 27 ++----- - 7 files changed, 155 insertions(+), 120 deletions(-) - create mode 100644 CHANGES/8280.bugfix.rst - create mode 100644 CHANGES/8280.deprecation.rst - -diff --git a/CHANGES/8280.bugfix.rst b/CHANGES/8280.bugfix.rst -new file mode 100644 -index 00000000000..3aebe36fe9e ---- /dev/null -+++ b/CHANGES/8280.bugfix.rst -@@ -0,0 +1 @@ -+Fixed ``multipart/form-data`` compliance with :rfc:`7578` -- by :user:`Dreamsorcerer`. -diff --git a/CHANGES/8280.deprecation.rst b/CHANGES/8280.deprecation.rst -new file mode 100644 -index 00000000000..302dbb2fe2a ---- /dev/null -+++ b/CHANGES/8280.deprecation.rst -@@ -0,0 +1,2 @@ -+Deprecated ``content_transfer_encoding`` parameter in :py:meth:`FormData.add_field() -+<aiohttp.FormData.add_field>` -- by :user:`Dreamsorcerer`. -diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py -index e7cd24ca9f7..2b75b3de72c 100644 ---- a/aiohttp/formdata.py -+++ b/aiohttp/formdata.py -@@ -1,4 +1,5 @@ - import io -+import warnings - from typing import Any, Iterable, List, Optional - from urllib.parse import urlencode - -@@ -53,7 +54,12 @@ def add_field( - if isinstance(value, io.IOBase): - self._is_multipart = True - elif isinstance(value, (bytes, bytearray, memoryview)): -+ msg = ( -+ "In v4, passing bytes will no longer create a file field. " -+ "Please explicitly use the filename parameter or pass a BytesIO object." -+ ) - if filename is None and content_transfer_encoding is None: -+ warnings.warn(msg, DeprecationWarning) - filename = name - - type_options: MultiDict[str] = MultiDict({"name": name}) -@@ -81,7 +87,11 @@ def add_field( - "content_transfer_encoding must be an instance" - " of str. Got: %s" % content_transfer_encoding - ) -- headers[hdrs.CONTENT_TRANSFER_ENCODING] = content_transfer_encoding -+ msg = ( -+ "content_transfer_encoding is deprecated. " -+ "To maintain compatibility with v4 please pass a BytesPayload." -+ ) -+ warnings.warn(msg, DeprecationWarning) - self._is_multipart = True - - self._fields.append((type_options, headers, value)) -diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py -index 4471dd4bb7e..a43ec545713 100644 ---- a/aiohttp/multipart.py -+++ b/aiohttp/multipart.py -@@ -256,13 +256,22 @@ class BodyPartReader: - chunk_size = 8192 - - def __init__( -- self, boundary: bytes, headers: "CIMultiDictProxy[str]", content: StreamReader -+ self, -+ boundary: bytes, -+ headers: "CIMultiDictProxy[str]", -+ content: StreamReader, -+ *, -+ subtype: str = "mixed", -+ default_charset: Optional[str] = None, - ) -> None: - self.headers = headers - self._boundary = boundary - self._content = content -+ self._default_charset = default_charset - self._at_eof = False -- length = self.headers.get(CONTENT_LENGTH, None) -+ self._is_form_data = subtype == "form-data" -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 -+ length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) - self._length = int(length) if length is not None else None - self._read_bytes = 0 - self._unread: Deque[bytes] = deque() -@@ -329,6 +338,8 @@ async def _read_chunk_from_length(self, size: int) -> bytes: - assert self._length is not None, "Content-Length required for chunked read" - chunk_size = min(size, self._length - self._read_bytes) - chunk = await self._content.read(chunk_size) -+ if self._content.at_eof(): -+ self._at_eof = True - return chunk - - async def _read_chunk_from_stream(self, size: int) -> bytes: -@@ -449,7 +460,8 @@ def decode(self, data: bytes) -> bytes: - """ - if CONTENT_TRANSFER_ENCODING in self.headers: - data = self._decode_content_transfer(data) -- if CONTENT_ENCODING in self.headers: -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 -+ if not self._is_form_data and CONTENT_ENCODING in self.headers: - return self._decode_content(data) - return data - -@@ -483,7 +495,7 @@ def get_charset(self, default: str) -> str: - """Returns charset parameter from Content-Type header or default.""" - ctype = self.headers.get(CONTENT_TYPE, "") - mimetype = parse_mimetype(ctype) -- return mimetype.parameters.get("charset", default) -+ return mimetype.parameters.get("charset", self._default_charset or default) - - @reify - def name(self) -> Optional[str]: -@@ -538,9 +550,17 @@ class MultipartReader: - part_reader_cls = BodyPartReader - - def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None: -+ self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) -+ assert self._mimetype.type == "multipart", "multipart/* content type expected" -+ if "boundary" not in self._mimetype.parameters: -+ raise ValueError( -+ "boundary missed for Content-Type: %s" % headers[CONTENT_TYPE] -+ ) -+ - self.headers = headers - self._boundary = ("--" + self._get_boundary()).encode() - self._content = content -+ self._default_charset: Optional[str] = None - self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None - self._at_eof = False - self._at_bof = True -@@ -592,7 +612,24 @@ async def next( - await self._read_boundary() - if self._at_eof: # we just read the last boundary, nothing to do there - return None -- self._last_part = await self.fetch_next_part() -+ -+ part = await self.fetch_next_part() -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.6 -+ if ( -+ self._last_part is None -+ and self._mimetype.subtype == "form-data" -+ and isinstance(part, BodyPartReader) -+ ): -+ _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION)) -+ if params.get("name") == "_charset_": -+ # Longest encoding in https://encoding.spec.whatwg.org/encodings.json -+ # is 19 characters, so 32 should be more than enough for any valid encoding. -+ charset = await part.read_chunk(32) -+ if len(charset) > 31: -+ raise RuntimeError("Invalid default charset") -+ self._default_charset = charset.strip().decode() -+ part = await self.fetch_next_part() -+ self._last_part = part - return self._last_part - - async def release(self) -> None: -@@ -628,19 +665,16 @@ def _get_part_reader( - return type(self)(headers, self._content) - return self.multipart_reader_cls(headers, self._content) - else: -- return self.part_reader_cls(self._boundary, headers, self._content) -- -- def _get_boundary(self) -> str: -- mimetype = parse_mimetype(self.headers[CONTENT_TYPE]) -- -- assert mimetype.type == "multipart", "multipart/* content type expected" -- -- if "boundary" not in mimetype.parameters: -- raise ValueError( -- "boundary missed for Content-Type: %s" % self.headers[CONTENT_TYPE] -+ return self.part_reader_cls( -+ self._boundary, -+ headers, -+ self._content, -+ subtype=self._mimetype.subtype, -+ default_charset=self._default_charset, - ) - -- boundary = mimetype.parameters["boundary"] -+ def _get_boundary(self) -> str: -+ boundary = self._mimetype.parameters["boundary"] - if len(boundary) > 70: - raise ValueError("boundary %r is too long (70 chars max)" % boundary) - -@@ -731,6 +765,7 @@ def __init__(self, subtype: str = "mixed", boundary: Optional[str] = None) -> No - super().__init__(None, content_type=ctype) - - self._parts: List[_Part] = [] -+ self._is_form_data = subtype == "form-data" - - def __enter__(self) -> "MultipartWriter": - return self -@@ -808,32 +843,36 @@ def append(self, obj: Any, headers: Optional[Mapping[str, str]] = None) -> Paylo - - def append_payload(self, payload: Payload) -> Payload: - """Adds a new body part to multipart writer.""" -- # compression -- encoding: Optional[str] = payload.headers.get( -- CONTENT_ENCODING, -- "", -- ).lower() -- if encoding and encoding not in ("deflate", "gzip", "identity"): -- raise RuntimeError(f"unknown content encoding: {encoding}") -- if encoding == "identity": -- encoding = None -- -- # te encoding -- te_encoding: Optional[str] = payload.headers.get( -- CONTENT_TRANSFER_ENCODING, -- "", -- ).lower() -- if te_encoding not in ("", "base64", "quoted-printable", "binary"): -- raise RuntimeError( -- "unknown content transfer encoding: {}" "".format(te_encoding) -+ encoding: Optional[str] = None -+ te_encoding: Optional[str] = None -+ if self._is_form_data: -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7 -+ # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 -+ assert CONTENT_DISPOSITION in payload.headers -+ assert "name=" in payload.headers[CONTENT_DISPOSITION] -+ assert ( -+ not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} -+ & payload.headers.keys() - ) -- if te_encoding == "binary": -- te_encoding = None -- -- # size -- size = payload.size -- if size is not None and not (encoding or te_encoding): -- payload.headers[CONTENT_LENGTH] = str(size) -+ else: -+ # compression -+ encoding = payload.headers.get(CONTENT_ENCODING, "").lower() -+ if encoding and encoding not in ("deflate", "gzip", "identity"): -+ raise RuntimeError(f"unknown content encoding: {encoding}") -+ if encoding == "identity": -+ encoding = None -+ -+ # te encoding -+ te_encoding = payload.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() -+ if te_encoding not in ("", "base64", "quoted-printable", "binary"): -+ raise RuntimeError(f"unknown content transfer encoding: {te_encoding}") -+ if te_encoding == "binary": -+ te_encoding = None -+ -+ # size -+ size = payload.size -+ if size is not None and not (encoding or te_encoding): -+ payload.headers[CONTENT_LENGTH] = str(size) - - self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type] - return payload -diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py -index 8a9a4e184be..dbb2dff5ac4 100644 ---- a/tests/test_client_functional.py -+++ b/tests/test_client_functional.py -@@ -1317,48 +1317,6 @@ async def handler(request): - resp.close() - - --async def test_POST_DATA_with_context_transfer_encoding(aiohttp_client) -> None: -- async def handler(request): -- data = await request.post() -- assert data["name"] == "text" -- return web.Response(text=data["name"]) -- -- app = web.Application() -- app.router.add_post("/", handler) -- client = await aiohttp_client(app) -- -- form = aiohttp.FormData() -- form.add_field("name", "text", content_transfer_encoding="base64") -- -- resp = await client.post("/", data=form) -- assert 200 == resp.status -- content = await resp.text() -- assert content == "text" -- resp.close() -- -- --async def test_POST_DATA_with_content_type_context_transfer_encoding(aiohttp_client): -- async def handler(request): -- data = await request.post() -- assert data["name"] == "text" -- return web.Response(body=data["name"]) -- -- app = web.Application() -- app.router.add_post("/", handler) -- client = await aiohttp_client(app) -- -- form = aiohttp.FormData() -- form.add_field( -- "name", "text", content_type="text/plain", content_transfer_encoding="base64" -- ) -- -- resp = await client.post("/", data=form) -- assert 200 == resp.status -- content = await resp.text() -- assert content == "text" -- resp.close() -- -- - async def test_POST_MultiDict(aiohttp_client) -> None: - async def handler(request): - data = await request.post() -@@ -1410,7 +1368,7 @@ async def handler(request): - - with fname.open("rb") as f: - async with client.post( -- "/", data={"some": f, "test": b"data"}, chunked=True -+ "/", data={"some": f, "test": io.BytesIO(b"data")}, chunked=True - ) as resp: - assert 200 == resp.status - -diff --git a/tests/test_multipart.py b/tests/test_multipart.py -index f9d130e7949..dbfaf74b9b7 100644 ---- a/tests/test_multipart.py -+++ b/tests/test_multipart.py -@@ -944,6 +944,58 @@ async def test_reading_skips_prelude(self) -> None: - assert first.at_eof() - assert not second.at_eof() - -+ async def test_read_form_default_encoding(self) -> None: -+ with Stream( -+ b"--:\r\n" -+ b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' -+ b"ascii" -+ b"\r\n" -+ b"--:\r\n" -+ b'Content-Disposition: form-data; name="field1"\r\n\r\n' -+ b"foo" -+ b"\r\n" -+ b"--:\r\n" -+ b"Content-Type: text/plain;charset=UTF-8\r\n" -+ b'Content-Disposition: form-data; name="field2"\r\n\r\n' -+ b"foo" -+ b"\r\n" -+ b"--:\r\n" -+ b'Content-Disposition: form-data; name="field3"\r\n\r\n' -+ b"foo" -+ b"\r\n" -+ ) as stream: -+ reader = aiohttp.MultipartReader( -+ {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, -+ stream, -+ ) -+ field1 = await reader.next() -+ assert field1.name == "field1" -+ assert field1.get_charset("default") == "ascii" -+ field2 = await reader.next() -+ assert field2.name == "field2" -+ assert field2.get_charset("default") == "UTF-8" -+ field3 = await reader.next() -+ assert field3.name == "field3" -+ assert field3.get_charset("default") == "ascii" -+ -+ async def test_read_form_invalid_default_encoding(self) -> None: -+ with Stream( -+ b"--:\r\n" -+ b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' -+ b"this-value-is-too-long-to-be-a-charset" -+ b"\r\n" -+ b"--:\r\n" -+ b'Content-Disposition: form-data; name="field1"\r\n\r\n' -+ b"foo" -+ b"\r\n" -+ ) as stream: -+ reader = aiohttp.MultipartReader( -+ {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, -+ stream, -+ ) -+ with pytest.raises(RuntimeError, match="Invalid default charset"): -+ await reader.next() -+ - - async def test_writer(writer) -> None: - assert writer.size == 7 -@@ -1280,7 +1332,6 @@ async def test_preserve_content_disposition_header(self, buf, stream): - CONTENT_TYPE: "text/python", - }, - ) -- content_length = part.size - await writer.write(stream) - - assert part.headers[CONTENT_TYPE] == "text/python" -@@ -1291,9 +1342,7 @@ async def test_preserve_content_disposition_header(self, buf, stream): - assert headers == ( - b"--:\r\n" - b"Content-Type: text/python\r\n" -- b'Content-Disposition: attachments; filename="bug.py"\r\n' -- b"Content-Length: %s" -- b"" % (str(content_length).encode(),) -+ b'Content-Disposition: attachments; filename="bug.py"' - ) - - async def test_set_content_disposition_override(self, buf, stream): -@@ -1307,7 +1356,6 @@ async def test_set_content_disposition_override(self, buf, stream): - CONTENT_TYPE: "text/python", - }, - ) -- content_length = part.size - await writer.write(stream) - - assert part.headers[CONTENT_TYPE] == "text/python" -@@ -1318,9 +1366,7 @@ async def test_set_content_disposition_override(self, buf, stream): - assert headers == ( - b"--:\r\n" - b"Content-Type: text/python\r\n" -- b'Content-Disposition: attachments; filename="bug.py"\r\n' -- b"Content-Length: %s" -- b"" % (str(content_length).encode(),) -+ b'Content-Disposition: attachments; filename="bug.py"' - ) - - async def test_reset_content_disposition_header(self, buf, stream): -@@ -1332,8 +1378,6 @@ async def test_reset_content_disposition_header(self, buf, stream): - headers={CONTENT_TYPE: "text/plain"}, - ) - -- content_length = part.size -- - assert CONTENT_DISPOSITION in part.headers - - part.set_content_disposition("attachments", filename="bug.py") -@@ -1346,9 +1390,7 @@ async def test_reset_content_disposition_header(self, buf, stream): - b"--:\r\n" - b"Content-Type: text/plain\r\n" - b"Content-Disposition:" -- b' attachments; filename="bug.py"\r\n' -- b"Content-Length: %s" -- b"" % (str(content_length).encode(),) -+ b' attachments; filename="bug.py"' - ) - - -diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py -index 04fc2e35fd1..ee61537068b 100644 ---- a/tests/test_web_functional.py -+++ b/tests/test_web_functional.py -@@ -48,7 +48,8 @@ def fname(here): - - def new_dummy_form(): - form = FormData() -- form.add_field("name", b"123", content_transfer_encoding="base64") -+ with pytest.warns(DeprecationWarning, match="BytesPayload"): -+ form.add_field("name", b"123", content_transfer_encoding="base64") - return form - - -@@ -447,25 +448,6 @@ async def handler(request): - await resp.release() - - --async def test_POST_DATA_with_content_transfer_encoding(aiohttp_client) -> None: -- async def handler(request): -- data = await request.post() -- assert b"123" == data["name"] -- return web.Response() -- -- app = web.Application() -- app.router.add_post("/", handler) -- client = await aiohttp_client(app) -- -- form = FormData() -- form.add_field("name", b"123", content_transfer_encoding="base64") -- -- resp = await client.post("/", data=form) -- assert 200 == resp.status -- -- await resp.release() -- -- - async def test_post_form_with_duplicate_keys(aiohttp_client) -> None: - async def handler(request): - data = await request.post() -@@ -523,7 +505,8 @@ async def handler(request): - return web.Response() - - form = FormData() -- form.add_field("name", b"123", content_transfer_encoding="base64") -+ with pytest.warns(DeprecationWarning, match="BytesPayload"): -+ form.add_field("name", b"123", content_transfer_encoding="base64") - - app = web.Application() - app.router.add_post("/", handler) -@@ -727,7 +710,7 @@ async def handler(request): - app.router.add_post("/", handler) - client = await aiohttp_client(app) - -- resp = await client.post("/", data={"file": data}) -+ resp = await client.post("/", data={"file": io.BytesIO(data)}) - assert 200 == resp.status - - await resp.release() diff --git a/CVE-2024-42367.patch b/CVE-2024-42367.patch deleted file mode 100644 index 2e1b42696697ef259c9a312c0e42c565dda87435..0000000000000000000000000000000000000000 --- a/CVE-2024-42367.patch +++ /dev/null @@ -1,198 +0,0 @@ -From f98240ad2279c3e97b65eddce40d37948f383416 Mon Sep 17 00:00:00 2001 -From: "J. Nick Koston" <nick@koston.org> -Date: Thu, 8 Aug 2024 11:19:28 -0500 -Subject: [PATCH] Do not follow symlinks for compressed file variants (#8652) - -Co-authored-by: Steve Repsher <steverep@users.noreply.github.com> -(cherry picked from commit b0536ae6babf160105d4025ea87c02b9fa5629f1) ---- - CHANGES/8652.bugfix.rst | 1 + - aiohttp/web_fileresponse.py | 9 ++++++-- - tests/test_web_sendfile.py | 37 ++++++++++++++++++++------------- - tests/test_web_urldispatcher.py | 32 ++++++++++++++++++++++++++++ - 4 files changed, 63 insertions(+), 16 deletions(-) - create mode 100644 CHANGES/8652.bugfix.rst - -diff --git a/CHANGES/8652.bugfix.rst b/CHANGES/8652.bugfix.rst -new file mode 100644 -index 0000000..3a1003e ---- /dev/null -+++ b/CHANGES/8652.bugfix.rst -@@ -0,0 +1 @@ -+Fixed incorrectly following symlinks for compressed file variants -- by :user:`steverep`. -diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py -index 6496ffa..acb0579 100644 ---- a/aiohttp/web_fileresponse.py -+++ b/aiohttp/web_fileresponse.py -@@ -2,6 +2,7 @@ import asyncio - import mimetypes - import os - import pathlib -+from stat import S_ISREG - from typing import ( # noqa - IO, - TYPE_CHECKING, -@@ -136,12 +137,16 @@ class FileResponse(StreamResponse): - if check_for_gzipped_file: - gzip_path = filepath.with_name(filepath.name + ".gz") - try: -- return gzip_path, gzip_path.stat(), True -+ st = gzip_path.lstat() -+ if S_ISREG(st.st_mode): -+ return gzip_path, st, True - except OSError: - # Fall through and try the non-gzipped file - pass - -- return filepath, filepath.stat(), False -+ st = filepath.lstat() -+ if S_ISREG(st.st_mode): -+ return filepath, st, False - - async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: - loop = asyncio.get_event_loop() -diff --git a/tests/test_web_sendfile.py b/tests/test_web_sendfile.py -index 2817e08..da9e6ae 100644 ---- a/tests/test_web_sendfile.py -+++ b/tests/test_web_sendfile.py -@@ -1,10 +1,13 @@ - from pathlib import Path -+from stat import S_IFREG, S_IRUSR, S_IWUSR - from unittest import mock - - from aiohttp import hdrs - from aiohttp.test_utils import make_mocked_coro, make_mocked_request - from aiohttp.web_fileresponse import FileResponse - -+MOCK_MODE = S_IFREG | S_IRUSR | S_IWUSR -+ - - def test_using_gzip_if_header_present_and_file_available(loop) -> None: - request = make_mocked_request( -@@ -12,8 +15,9 @@ def test_using_gzip_if_header_present_and_file_available(loop) -> None: - ) - - gz_filepath = mock.create_autospec(Path, spec_set=True) -- gz_filepath.stat.return_value.st_size = 1024 -- gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ gz_filepath.lstat.return_value.st_size = 1024 -+ gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ gz_filepath.lstat.return_value.st_mode = MOCK_MODE - - filepath = mock.create_autospec(Path, spec_set=True) - filepath.name = "logo.png" -@@ -33,14 +37,16 @@ def test_gzip_if_header_not_present_and_file_available(loop) -> None: - request = make_mocked_request("GET", "http://python.org/logo.png", headers={}) - - gz_filepath = mock.create_autospec(Path, spec_set=True) -- gz_filepath.stat.return_value.st_size = 1024 -- gz_filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ gz_filepath.lstat.return_value.st_size = 1024 -+ gz_filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ gz_filepath.lstat.return_value.st_mode = MOCK_MODE - - filepath = mock.create_autospec(Path, spec_set=True) - filepath.name = "logo.png" - filepath.with_name.return_value = gz_filepath -- filepath.stat.return_value.st_size = 1024 -- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_size = 1024 -+ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_mode = MOCK_MODE - - file_sender = FileResponse(filepath) - file_sender._path = filepath -@@ -56,13 +62,14 @@ def test_gzip_if_header_not_present_and_file_not_available(loop) -> None: - request = make_mocked_request("GET", "http://python.org/logo.png", headers={}) - - gz_filepath = mock.create_autospec(Path, spec_set=True) -- gz_filepath.stat.side_effect = OSError(2, "No such file or directory") -+ gz_filepath.lstat.side_effect = OSError(2, "No such file or directory") - - filepath = mock.create_autospec(Path, spec_set=True) - filepath.name = "logo.png" - filepath.with_name.return_value = gz_filepath -- filepath.stat.return_value.st_size = 1024 -- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_size = 1024 -+ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_mode = MOCK_MODE - - file_sender = FileResponse(filepath) - file_sender._path = filepath -@@ -80,13 +87,14 @@ def test_gzip_if_header_present_and_file_not_available(loop) -> None: - ) - - gz_filepath = mock.create_autospec(Path, spec_set=True) -- gz_filepath.stat.side_effect = OSError(2, "No such file or directory") -+ gz_filepath.lstat.side_effect = OSError(2, "No such file or directory") - - filepath = mock.create_autospec(Path, spec_set=True) - filepath.name = "logo.png" - filepath.with_name.return_value = gz_filepath -- filepath.stat.return_value.st_size = 1024 -- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_size = 1024 -+ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_mode = MOCK_MODE - - file_sender = FileResponse(filepath) - file_sender._path = filepath -@@ -103,8 +111,9 @@ def test_status_controlled_by_user(loop) -> None: - - filepath = mock.create_autospec(Path, spec_set=True) - filepath.name = "logo.png" -- filepath.stat.return_value.st_size = 1024 -- filepath.stat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_size = 1024 -+ filepath.lstat.return_value.st_mtime_ns = 1603733507222449291 -+ filepath.lstat.return_value.st_mode = MOCK_MODE - - file_sender = FileResponse(filepath, status=203) - file_sender._path = filepath -diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py -index 0441890..62cb0de 100644 ---- a/tests/test_web_urldispatcher.py -+++ b/tests/test_web_urldispatcher.py -@@ -440,6 +440,38 @@ async def test_access_symlink_loop( - assert r.status == 404 - - -+async def test_access_compressed_file_as_symlink( -+ tmp_path: pathlib.Path, aiohttp_client: AiohttpClient -+) -> None: -+ """Test that compressed file variants as symlinks are ignored.""" -+ private_file = tmp_path / "private.txt" -+ private_file.write_text("private info") -+ www_dir = tmp_path / "www" -+ www_dir.mkdir() -+ gz_link = www_dir / "file.txt.gz" -+ gz_link.symlink_to(f"../{private_file.name}") -+ -+ app = web.Application() -+ app.router.add_static("/", www_dir) -+ client = await aiohttp_client(app) -+ -+ # Symlink should be ignored; response reflects missing uncompressed file. -+ resp = await client.get(f"/{gz_link.stem}", auto_decompress=False) -+ assert resp.status == 404 -+ resp.release() -+ -+ # Again symlin is ignored, and then uncompressed is served. -+ txt_file = gz_link.with_suffix("") -+ txt_file.write_text("public data") -+ resp = await client.get(f"/{txt_file.name}") -+ assert resp.status == 200 -+ assert resp.headers.get("Content-Encoding") is None -+ assert resp.content_type == "text/plain" -+ assert await resp.text() == "public data" -+ resp.release() -+ await client.close() -+ -+ - async def test_access_special_resource( - tmp_path: pathlib.Path, aiohttp_client: AiohttpClient - ) -> None: --- -2.33.0 - diff --git a/Fix-Python-parser-to-mark-responses-without-length-a.patch b/Fix-Python-parser-to-mark-responses-without-length-a.patch deleted file mode 100644 index 1e8bd2bc06eb8a3756eefa9af4aace6764144ea8..0000000000000000000000000000000000000000 --- a/Fix-Python-parser-to-mark-responses-without-length-a.patch +++ /dev/null @@ -1,57 +0,0 @@ -From 3223e1209285d96cfe5ac92c68653c5690e6e721 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?=E8=82=96=E5=9C=A8?= <xiaozai@kylinos.cn> -Date: Mon, 6 May 2024 20:30:09 +0800 -Subject: [PATCH] Fix Python parser to mark responses without length as closing - ---- - CHANGES/8320.bugfix.rst | 1 + - aiohttp/http_parser.py | 11 ++++++++++- - tests/test_http_parser.py | 2 +- - 3 files changed, 12 insertions(+), 2 deletions(-) - create mode 100644 CHANGES/8320.bugfix.rst - -diff --git a/CHANGES/8320.bugfix.rst b/CHANGES/8320.bugfix.rst -new file mode 100644 -index 0000000..3823e24 ---- /dev/null -+++ b/CHANGES/8320.bugfix.rst -@@ -0,0 +1 @@ -+Fixed the pure python parser to mark a connection as closing when a response has no length -- by :user:`Dreamsorcerer` -diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index 1877f55..d7b8dac 100644 ---- a/aiohttp/http_parser.py -+++ b/aiohttp/http_parser.py -@@ -703,7 +703,16 @@ class HttpResponseParser(HttpParser[RawResponseMessage]): - ) = self.parse_headers(lines) - - if close is None: -- close = version_o <= HttpVersion10 -+ if version_o <= HttpVersion10: -+ close = True -+ # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length -+ elif 100 <= status_i < 200 or status_i in {204, 304}: -+ close = False -+ elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers: -+ close = False -+ else: -+ # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8 -+ close = True - - return RawResponseMessage( - version_o, -diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index b931730..0417fa4 100644 ---- a/tests/test_http_parser.py -+++ b/tests/test_http_parser.py -@@ -743,7 +743,7 @@ def test_http_request_parser(parser) -> None: - assert msg.version == (1, 1) - assert msg.headers == CIMultiDict() - assert msg.raw_headers == () -- assert not msg.should_close -+ assert msg.should_close - assert msg.compression is None - assert not msg.upgrade - assert not msg.chunked --- -2.33.0 - diff --git a/aiohttp-3.9.3.tar.gz b/aiohttp-3.12.14.tar.gz similarity index 49% rename from aiohttp-3.9.3.tar.gz rename to aiohttp-3.12.14.tar.gz index 863954bb448419a5eaaa849431290b5e9249f95a..f26c947432cc6da24a00ec0b4f959d003efc9e12 100644 Binary files a/aiohttp-3.9.3.tar.gz and b/aiohttp-3.12.14.tar.gz differ diff --git a/python-aiohttp.spec b/python-aiohttp.spec index 145006b53bbf236be2f4a0129ad5e523413fb6c0..c17ca6af30a9599b91d843a917868c9b63e5336d 100644 --- a/python-aiohttp.spec +++ b/python-aiohttp.spec @@ -1,22 +1,11 @@ %global _empty_manifest_terminate_build 0 Name: python-aiohttp -Version: 3.9.3 -Release: 5 +Version: 3.12.14 +Release: 1 Summary: Async http client/server framework (asyncio) License: Apache 2 URL: https://github.com/aio-libs/aiohttp Source0: %{pypi_source aiohttp} -# https://github.com/aio-libs/aiohttp/commit/28335525d1eac015a7e7584137678cbb6ff19397 -Patch0: CVE-2024-27306.patch -# https://github.com/aio-libs/aiohttp/commit/cebe526b9c34dc3a3da9140409db63014bc4cf19 -Patch1: CVE-2024-30251.patch -# https://github.com/aio-libs/aiohttp/commit/7eecdff163ccf029fbb1ddc9de4169d4aaeb6597 -Patch2: CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch -# https://github.com/aio-libs/aiohttp/commit/f21c6f2ca512a026ce7f0f6c6311f62d6a638866 -Patch3: CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch -# https://github.com/aio-libs/aiohttp/commit/9ba9a4e531599b9cb2f8cc80effbde40c7eab0bd -Patch4: Fix-Python-parser-to-mark-responses-without-length-a.patch -Patch5: CVE-2024-42367.patch Requires: python3-attrs Requires: python3-charset-normalizer @@ -92,6 +81,9 @@ mv %{buildroot}/doclist.lst . %{_docdir}/* %changelog +* Thu Jul 17 2025 Yu Peng <yupeng@kylinos.cn> - 3.12.14-1 +- Upgrade to 3.12.14 for fix CVE-2025-53643. + * Fri Oct 11 2024 yaoxin <yao_xin001@hoperun.com> - 3.9.3-5 - Fix CVE-2024-42367