diff --git a/CVE-2023-47627.patch b/CVE-2023-47627.patch deleted file mode 100644 index d32c708a1098b336c55420b450e62247bd181d5a..0000000000000000000000000000000000000000 --- a/CVE-2023-47627.patch +++ /dev/null @@ -1,353 +0,0 @@ -From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> -Date: Fri, 6 Oct 2023 17:11:40 +0100 -Subject: Update Python parser for RFCs 9110/9112 (#7662) - -**This is a backport of PR #7661 as merged into 3.9 -(85713a4894610e848490915e5871ad71199348e2).** - -None - -Co-authored-by: Sam Bull ---- - aiohttp/http_parser.py | 85 ++++++++++++++++++++++++---------------- - tests/test_http_parser.py | 99 +++++++++++++++++++++++++++++++++++++++++++++-- - 2 files changed, 146 insertions(+), 38 deletions(-) - -diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index 71ba815..0406bf4 100644 ---- a/aiohttp/http_parser.py -+++ b/aiohttp/http_parser.py -@@ -5,7 +5,7 @@ import re - import string - import zlib - from enum import IntEnum --from typing import Any, List, Optional, Tuple, Type, Union -+from typing import Any, Final, List, Optional, Pattern, Tuple, Type, Union - - from multidict import CIMultiDict, CIMultiDictProxy, istr - from yarl import URL -@@ -45,16 +45,16 @@ __all__ = ( - - ASCIISET = set(string.printable) - --# See https://tools.ietf.org/html/rfc7230#section-3.1.1 --# and https://tools.ietf.org/html/rfc7230#appendix-B -+# See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview -+# and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens - # - # method = token - # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / - # "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA - # token = 1*tchar - METHRE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") --VERSRE = re.compile(r"HTTP/(\d+).(\d+)") --HDRRE = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]") -+VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d).(\d)") -+HDRRE: Final[Pattern[bytes]] = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\"\\]") - - RawRequestMessage = collections.namedtuple( - "RawRequestMessage", -@@ -132,8 +132,11 @@ class HeadersParser: - except ValueError: - raise InvalidHeader(line) from None - -- bname = bname.strip(b" \t") -- bvalue = bvalue.lstrip() -+ # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 -+ if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"} -+ raise InvalidHeader(line) -+ -+ bvalue = bvalue.lstrip(b" \t") - if HDRRE.search(bname): - raise InvalidHeader(bname) - if len(bname) > self.max_field_size: -@@ -154,6 +157,7 @@ class HeadersParser: - # consume continuation lines - continuation = line and line[0] in (32, 9) # (' ', '\t') - -+ # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding - if continuation: - bvalue_lst = [bvalue] - while continuation: -@@ -188,10 +192,14 @@ class HeadersParser: - str(header_length), - ) - -- bvalue = bvalue.strip() -+ bvalue = bvalue.strip(b" \t") - name = bname.decode("utf-8", "surrogateescape") - value = bvalue.decode("utf-8", "surrogateescape") - -+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 -+ if "\n" in value or "\r" in value or "\x00" in value: -+ raise InvalidHeader(bvalue) -+ - headers.add(name, value) - raw_headers.append((bname, bvalue)) - -@@ -304,13 +312,13 @@ class HttpParser(abc.ABC): - # payload length - length = msg.headers.get(CONTENT_LENGTH) - if length is not None: -- try: -- length = int(length) -- except ValueError: -- raise InvalidHeader(CONTENT_LENGTH) -- if length < 0: -+ # Shouldn't allow +/- or other number formats. -+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 -+ if not length.strip(" \t").isdigit(): - raise InvalidHeader(CONTENT_LENGTH) - -+ length = int(length) -+ - # do not support old websocket spec - if SEC_WEBSOCKET_KEY1 in msg.headers: - raise InvalidHeader(SEC_WEBSOCKET_KEY1) -@@ -447,6 +455,24 @@ class HttpParser(abc.ABC): - upgrade = False - chunked = False - -+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-6 -+ # https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf -+ singletons = ( -+ hdrs.CONTENT_LENGTH, -+ hdrs.CONTENT_LOCATION, -+ hdrs.CONTENT_RANGE, -+ hdrs.CONTENT_TYPE, -+ hdrs.ETAG, -+ hdrs.HOST, -+ hdrs.MAX_FORWARDS, -+ hdrs.SERVER, -+ hdrs.TRANSFER_ENCODING, -+ hdrs.USER_AGENT, -+ ) -+ bad_hdr = next((h for h in singletons if len(headers.getall(h, ())) > 1), None) -+ if bad_hdr is not None: -+ raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.") -+ - # keep-alive - conn = headers.get(hdrs.CONNECTION) - if conn: -@@ -489,7 +515,7 @@ class HttpRequestParser(HttpParser): - # request line - line = lines[0].decode("utf-8", "surrogateescape") - try: -- method, path, version = line.split(None, 2) -+ method, path, version = line.split(maxsplit=2) - except ValueError: - raise BadStatusLine(line) from None - -@@ -506,14 +532,10 @@ class HttpRequestParser(HttpParser): - raise BadStatusLine(method) - - # version -- try: -- if version.startswith("HTTP/"): -- n1, n2 = version[5:].split(".", 1) -- version_o = HttpVersion(int(n1), int(n2)) -- else: -- raise BadStatusLine(version) -- except Exception: -- raise BadStatusLine(version) -+ match = VERSRE.match(version) -+ if match is None: -+ raise BadStatusLine(line) -+ version_o = HttpVersion(int(match.group(1)), int(match.group(2))) - - # read headers - ( -@@ -563,12 +585,12 @@ class HttpResponseParser(HttpParser): - def parse_message(self, lines: List[bytes]) -> Any: - line = lines[0].decode("utf-8", "surrogateescape") - try: -- version, status = line.split(None, 1) -+ version, status = line.split(maxsplit=1) - except ValueError: - raise BadStatusLine(line) from None - - try: -- status, reason = status.split(None, 1) -+ status, reason = status.split(maxsplit=1) - except ValueError: - reason = "" - -@@ -584,13 +606,9 @@ class HttpResponseParser(HttpParser): - version_o = HttpVersion(int(match.group(1)), int(match.group(2))) - - # The status code is a three-digit number -- try: -- status_i = int(status) -- except ValueError: -- raise BadStatusLine(line) from None -- -- if status_i > 999: -+ if len(status) != 3 or not status.isdigit(): - raise BadStatusLine(line) -+ status_i = int(status) - - # read headers - ( -@@ -725,14 +743,13 @@ class HttpPayloadParser: - else: - size_b = chunk[:pos] - -- try: -- size = int(bytes(size_b), 16) -- except ValueError: -+ if not size_b.isdigit(): - exc = TransferEncodingError( - chunk[:pos].decode("ascii", "surrogateescape") - ) - self.payload.set_exception(exc) -- raise exc from None -+ raise exc -+ size = int(bytes(size_b), 16) - - chunk = chunk[pos + 2 :] - if size == 0: # eof marker -diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index ab39cdd..214afe8 100644 ---- a/tests/test_http_parser.py -+++ b/tests/test_http_parser.py -@@ -3,6 +3,7 @@ - import asyncio - from unittest import mock - from urllib.parse import quote -+from typing import Any - - import pytest - from multidict import CIMultiDict -@@ -365,6 +366,83 @@ def test_invalid_name(parser) -> None: - parser.feed_data(text) - - -+def test_cve_2023_37276(parser: Any) -> None: -+ text = b"""POST / HTTP/1.1\r\nHost: localhost:8080\r\nX-Abc: \rxTransfer-Encoding: chunked\r\n\r\n""" -+ if isinstance(parser, HttpRequestParserPy): -+ with pytest.raises(aiohttp.http_exceptions.InvalidHeader): -+ parser.feed_data(text) -+ else: -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ -+ -+@pytest.mark.parametrize( -+ "hdr", -+ ( -+ "Content-Length: -5", # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length -+ "Content-Length: +256", -+ "Foo: abc\rdef", # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 -+ "Bar: abc\ndef", -+ "Baz: abc\x00def", -+ "Foo : bar", # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 -+ "Foo\t: bar", -+ ), -+) -+def test_bad_headers(parser: Any, hdr: str) -> None: -+ text = f"POST / HTTP/1.1\r\n{hdr}\r\n\r\n".encode() -+ if isinstance(parser, HttpRequestParserPy): -+ with pytest.raises(aiohttp.http_exceptions.InvalidHeader): -+ parser.feed_data(text) -+ elif hdr != "Foo : bar": -+ with pytest.raises(aiohttp.http_exceptions.BadHttpMessage): -+ parser.feed_data(text) -+ else: -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ -+ -+def test_bad_chunked_py(loop: Any, protocol: Any) -> None: -+ """Test that invalid chunked encoding doesn't allow content-length to be used.""" -+ parser = HttpRequestParserPy( -+ protocol, -+ loop, -+ 2**16, -+ max_line_size=8190, -+ max_field_size=8190, -+ ) -+ text = ( -+ b"GET / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0_2e\r\n\r\n" -+ + b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\n0\r\n\r\n" -+ ) -+ messages, upgrade, tail = parser.feed_data(text) -+ assert isinstance(messages[0][1].exception(), http_exceptions.TransferEncodingError) -+ -+ -+@pytest.mark.skipif( -+ "HttpRequestParserC" not in dir(aiohttp.http_parser), -+ reason="C based HTTP parser not available", -+) -+def test_bad_chunked_c(loop: Any, protocol: Any) -> None: -+ """C parser behaves differently. Maybe we should align them later.""" -+ parser = HttpRequestParserC( -+ protocol, -+ loop, -+ 2**16, -+ max_line_size=8190, -+ max_field_size=8190, -+ ) -+ text = ( -+ b"GET / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0_2e\r\n\r\n" -+ + b"GET / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\n0\r\n\r\n" -+ ) -+ with pytest.raises(http_exceptions.BadHttpMessage): -+ parser.feed_data(text) -+ -+ -+def test_whitespace_before_header(parser: Any) -> None: -+ text = b"GET / HTTP/1.1\r\n\tContent-Length: 1\r\n\r\nX" -+ with pytest.raises(http_exceptions.BadHttpMessage): -+ parser.feed_data(text) -+ -+ - @pytest.mark.parametrize("size", [40960, 8191]) - def test_max_header_field_size(parser, size) -> None: - name = b"t" * size -@@ -546,6 +624,11 @@ def test_http_request_parser_bad_version(parser) -> None: - parser.feed_data(b"GET //get HT/11\r\n\r\n") - - -+def test_http_request_parser_bad_version_number(parser: Any) -> None: -+ with pytest.raises(http_exceptions.BadHttpMessage): -+ parser.feed_data(b"GET /test HTTP/12.3\r\n\r\n") -+ -+ - @pytest.mark.parametrize("size", [40965, 8191]) - def test_http_request_max_status_line(parser, size) -> None: - path = b"t" * (size - 5) -@@ -613,6 +696,11 @@ def test_http_response_parser_bad_version(response) -> None: - response.feed_data(b"HT/11 200 Ok\r\n\r\n") - - -+def test_http_response_parser_bad_version_number(response) -> None: -+ with pytest.raises(http_exceptions.BadHttpMessage): -+ response.feed_data(b"HTTP/12.3 200 Ok\r\n\r\n") -+ -+ - def test_http_response_parser_no_reason(response) -> None: - msg = response.feed_data(b"HTTP/1.1 200\r\n\r\n")[0][0][0] - -@@ -627,17 +715,20 @@ def test_http_response_parser_bad(response) -> None: - - - def test_http_response_parser_code_under_100(response) -> None: -- msg = response.feed_data(b"HTTP/1.1 99 test\r\n\r\n")[0][0][0] -- assert msg.code == 99 -+ if isinstance(response, HttpResponseParserPy): -+ with pytest.raises(aiohttp.http_exceptions.BadStatusLine): -+ response.feed_data(b"HTTP/1.1 99 test\r\n\r\n") -+ else: -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") - - - def test_http_response_parser_code_above_999(response) -> None: -- with pytest.raises(http_exceptions.BadHttpMessage): -+ with pytest.raises(http_exceptions.BadStatusLine): - response.feed_data(b"HTTP/1.1 9999 test\r\n\r\n") - - - def test_http_response_parser_code_not_int(response) -> None: -- with pytest.raises(http_exceptions.BadHttpMessage): -+ with pytest.raises(http_exceptions.BadStatusLine): - response.feed_data(b"HTTP/1.1 ttt test\r\n\r\n") - - diff --git a/CVE-2023-47641.patch b/CVE-2023-47641.patch deleted file mode 100644 index 2afc5b95010e03359029df769bcb7b52cc44cd6d..0000000000000000000000000000000000000000 --- a/CVE-2023-47641.patch +++ /dev/null @@ -1,77 +0,0 @@ -From f016f0680e4ace6742b03a70cb0382ce86abe371 Mon Sep 17 00:00:00 2001 -From: Andrew Svetlov -Date: Sun, 31 Oct 2021 19:03:06 +0200 -Subject: [PATCH] Raise '400: Content-Length can't be present with - Transfer-Encoding' if both Content-Length and Transfer-Encoding are sent by - peer (#6182) - ---- - CHANGES/6182.bugfix | 1 + - aiohttp/http_parser.py | 12 ++++++++++-- - tests/test_http_parser.py | 15 ++++++++++++++- - 3 files changed, 25 insertions(+), 3 deletions(-) - create mode 100644 CHANGES/6182.bugfix - -diff --git a/CHANGES/6182.bugfix b/CHANGES/6182.bugfix -new file mode 100644 -index 0000000000..28daaa328a ---- /dev/null -+++ b/CHANGES/6182.bugfix -@@ -0,0 +1 @@ -+Raise ``400: Content-Length can't be present with Transfer-Encoding`` if both ``Content-Length`` and ``Transfer-Encoding`` are sent by peer by both C and Python implementations -diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index 4a4ae31ae6..e1b86e8e4f 100644 ---- a/aiohttp/http_parser.py -+++ b/aiohttp/http_parser.py -@@ -28,6 +28,7 @@ - from .base_protocol import BaseProtocol - from .helpers import NO_EXTENSIONS, BaseTimerContext - from .http_exceptions import ( -+ BadHttpMessage, - BadStatusLine, - ContentEncodingError, - ContentLengthError, -@@ -489,8 +490,15 @@ def parse_headers( - - # chunking - te = headers.get(hdrs.TRANSFER_ENCODING) -- if te and "chunked" in te.lower(): -- chunked = True -+ if te is not None: -+ te_lower = te.lower() -+ if "chunked" in te_lower: -+ chunked = True -+ -+ if hdrs.CONTENT_LENGTH in headers: -+ raise BadHttpMessage( -+ "Content-Length can't be present with Transfer-Encoding", -+ ) - - return (headers, raw_headers, close_conn, encoding, upgrade, chunked) - -diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index 78e9ea6401..d86d238f58 100644 ---- a/tests/test_http_parser.py -+++ b/tests/test_http_parser.py -@@ -291,7 +291,20 @@ def test_request_chunked(parser) -> None: - assert isinstance(payload, streams.StreamReader) - - --def test_conn_upgrade(parser) -> None: -+def test_request_te_chunked_with_content_length(parser: Any) -> None: -+ text = ( -+ b"GET /test HTTP/1.1\r\n" -+ b"content-length: 1234\r\n" -+ b"transfer-encoding: chunked\r\n\r\n" -+ ) -+ with pytest.raises( -+ http_exceptions.BadHttpMessage, -+ match="Content-Length can't be present with Transfer-Encoding", -+ ): -+ parser.feed_data(text) -+ -+ -+def test_conn_upgrade(parser: Any) -> None: - text = ( - b"GET /test HTTP/1.1\r\n" - b"connection: upgrade\r\n" diff --git a/CVE-2023-49081.patch b/CVE-2023-49081.patch deleted file mode 100644 index 5294e21a98539d37c7d81d31ae4be0e2bb4bd5b0..0000000000000000000000000000000000000000 --- a/CVE-2023-49081.patch +++ /dev/null @@ -1,91 +0,0 @@ -From 53476dfd4ef4fb1bb74a267714bbc39eda71b403 Mon Sep 17 00:00:00 2001 -From: Sam Bull -Date: Mon, 13 Nov 2023 22:36:04 +0000 -Subject: [PATCH] Disallow arbitrary sequence types in version (#7835) (#7836) - -Origin: https://github.com/aio-libs/aiohttp/commit/53476dfd4ef4fb1bb74a267714bbc39eda71b403 - -(cherry picked from commit 1e86b777e61cf4eefc7d92fa57fa19dcc676013b) ---- - CHANGES/7835.bugfix | 1 + - aiohttp/client_reqrep.py | 4 ++-- - tests/test_client_request.py | 20 +++++++++++++++++--- - 3 files changed, 20 insertions(+), 5 deletions(-) - create mode 100644 CHANGES/7835.bugfix - -diff --git a/CHANGES/7835.bugfix b/CHANGES/7835.bugfix -new file mode 100644 -index 0000000000..4ce3af4f6f ---- /dev/null -+++ b/CHANGES/7835.bugfix -@@ -0,0 +1 @@ -+Fixed arbitrary sequence types being allowed to inject headers via version parameter -- by :user:`Dreamsorcerer` -diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py -index 851ab220b8..4cea7466d8 100644 ---- a/aiohttp/client_reqrep.py -+++ b/aiohttp/client_reqrep.py -@@ -706,8 +706,8 @@ async def send(self, conn: "Connection") -> "ClientResponse": - self.headers[hdrs.CONNECTION] = connection - - # status + headers -- status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format( -- self.method, path, self.version -+ status_line = "{0} {1} HTTP/{v.major}.{v.minor}".format( -+ self.method, path, v=self.version - ) - await writer.write_headers(status_line, self.headers) - -diff --git a/tests/test_client_request.py b/tests/test_client_request.py -index 0f58d752de..c8ce98d403 100644 ---- a/tests/test_client_request.py -+++ b/tests/test_client_request.py -@@ -21,6 +21,7 @@ - Fingerprint, - _merge_ssl_params, - ) -+from aiohttp.http import HttpVersion - from aiohttp.test_utils import make_mocked_coro - - -@@ -623,18 +624,18 @@ async def test_connection_header(loop, conn) -> None: - req.headers.clear() - - req.keep_alive.return_value = True -- req.version = (1, 1) -+ req.version = HttpVersion(1, 1) - req.headers.clear() - await req.send(conn) - assert req.headers.get("CONNECTION") is None - -- req.version = (1, 0) -+ req.version = HttpVersion(1, 0) - req.headers.clear() - await req.send(conn) - assert req.headers.get("CONNECTION") == "keep-alive" - - req.keep_alive.return_value = False -- req.version = (1, 1) -+ req.version = HttpVersion(1, 1) - req.headers.clear() - await req.send(conn) - assert req.headers.get("CONNECTION") == "close" -@@ -1161,6 +1162,19 @@ async def gen(): - resp.close() - - -+async def test_bad_version(loop, conn) -> None: -+ req = ClientRequest( -+ "GET", -+ URL("http://python.org"), -+ loop=loop, -+ headers={"Connection": "Close"}, -+ version=("1", "1\r\nInjected-Header: not allowed"), -+ ) -+ -+ with pytest.raises(AttributeError): -+ await req.send(conn) -+ -+ - async def test_custom_response_class(loop, conn) -> None: - class CustomResponse(ClientResponse): - def read(self, decode=False): diff --git a/CVE-2023-49082.patch b/CVE-2023-49082.patch deleted file mode 100644 index 6c4ba29217bb1b4157c83c6a09f70f4c567ec06e..0000000000000000000000000000000000000000 --- a/CVE-2023-49082.patch +++ /dev/null @@ -1,60 +0,0 @@ -From: Ben Kallus <49924171+kenballus@users.noreply.github.com> -Date: Wed, 18 Oct 2023 12:18:35 -0400 -Subject: Backport 493f06797654c383242f0e8007f6e06b818a1fbc to 3.9 (#7730) - ---- - aiohttp/http_parser.py | 6 ++++-- - tests/test_http_parser.py | 9 ++++++++- - 2 files changed, 12 insertions(+), 3 deletions(-) - -diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index 3862bbe..8e5e816 100644 ---- a/aiohttp/http_parser.py -+++ b/aiohttp/http_parser.py -@@ -55,7 +55,9 @@ ASCIISET = set(string.printable) - # token = 1*tchar - METHRE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") - VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d).(\d)") --HDRRE: Final[Pattern[bytes]] = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\"\\]") -+HDRRE: Final[Pattern[bytes]] = re.compile( -+ rb"[\x00-\x1F\x7F-\xFF()<>@,;:\[\]={} \t\"\\]" -+) - - RawRequestMessage = collections.namedtuple( - "RawRequestMessage", -@@ -523,7 +525,7 @@ class HttpRequestParser(HttpParser): - # request line - line = lines[0].decode("utf-8", "surrogateescape") - try: -- method, path, version = line.split(maxsplit=2) -+ method, path, version = line.split(" ", maxsplit=2) - except ValueError: - raise BadStatusLine(line) from None - -diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index d584f15..9d65b2f 100644 ---- a/tests/test_http_parser.py -+++ b/tests/test_http_parser.py -@@ -397,6 +397,7 @@ def test_cve_2023_37276(parser: Any) -> None: - "Baz: abc\x00def", - "Foo : bar", # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 - "Foo\t: bar", -+ "\xffoo: bar", - ), - ) - def test_bad_headers(parser: Any, hdr: str) -> None: -@@ -562,7 +563,13 @@ def test_http_request_bad_status_line(parser) -> None: - parser.feed_data(text) - - --def test_http_request_upgrade(parser) -> None: -+def test_http_request_bad_status_line_whitespace(parser: Any) -> None: -+ text = b"GET\n/path\fHTTP/1.1\r\n\r\n" -+ with pytest.raises(http_exceptions.BadStatusLine): -+ parser.feed_data(text) -+ -+ -+def test_http_request_upgrade(parser: Any) -> None: - text = ( - b"GET /test HTTP/1.1\r\n" - b"connection: upgrade\r\n" diff --git a/CVE-2024-23334.patch b/CVE-2024-23334.patch deleted file mode 100644 index ee7ba07f88a40ae3b5fd1c3f273c5c5fb1e8562c..0000000000000000000000000000000000000000 --- a/CVE-2024-23334.patch +++ /dev/null @@ -1,205 +0,0 @@ -From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> -Date: Sun, 28 Jan 2024 18:38:58 +0000 -Subject: Validate static paths (#8080) - -**This is a backport of PR #8079 as merged into master -(1c335944d6a8b1298baf179b7c0b3069f10c514b).** ---- - aiohttp/web_urldispatcher.py | 18 ++++++-- - docs/web_advanced.rst | 16 ++++++-- - docs/web_reference.rst | 12 ++++-- - tests/test_web_urldispatcher.py | 91 +++++++++++++++++++++++++++++++++++++++++ - 4 files changed, 127 insertions(+), 10 deletions(-) - -diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py -index 2afd72f..48557d5 100644 ---- a/aiohttp/web_urldispatcher.py -+++ b/aiohttp/web_urldispatcher.py -@@ -589,9 +589,14 @@ class StaticResource(PrefixResource): - url = url / filename - - if append_version: -+ unresolved_path = self._directory.joinpath(filename) - try: -- filepath = self._directory.joinpath(filename).resolve() -- if not self._follow_symlinks: -+ if self._follow_symlinks: -+ normalized_path = Path(os.path.normpath(unresolved_path)) -+ normalized_path.relative_to(self._directory) -+ filepath = normalized_path.resolve() -+ else: -+ filepath = unresolved_path.resolve() - filepath.relative_to(self._directory) - except (ValueError, FileNotFoundError): - # ValueError for case when path point to symlink -@@ -656,8 +661,13 @@ class StaticResource(PrefixResource): - # /static/\\machine_name\c$ or /static/D:\path - # where the static dir is totally different - raise HTTPForbidden() -- filepath = self._directory.joinpath(filename).resolve() -- if not self._follow_symlinks: -+ unresolved_path = self._directory.joinpath(filename) -+ if self._follow_symlinks: -+ normalized_path = Path(os.path.normpath(unresolved_path)) -+ normalized_path.relative_to(self._directory) -+ filepath = normalized_path.resolve() -+ else: -+ filepath = unresolved_path.resolve() - filepath.relative_to(self._directory) - except (ValueError, FileNotFoundError) as error: - # relatively safe -diff --git a/docs/web_advanced.rst b/docs/web_advanced.rst -index 01a3341..f5e082f 100644 ---- a/docs/web_advanced.rst -+++ b/docs/web_advanced.rst -@@ -136,12 +136,22 @@ instead could be enabled with ``show_index`` parameter set to ``True``:: - - web.static('/prefix', path_to_static_folder, show_index=True) - --When a symlink from the static directory is accessed, the server responses to --client with ``HTTP/404 Not Found`` by default. To allow the server to follow --symlinks, parameter ``follow_symlinks`` should be set to ``True``:: -+When a symlink that leads outside the static directory is accessed, the server -+responds to the client with ``HTTP/404 Not Found`` by default. To allow the server to -+follow symlinks that lead outside the static root, the parameter ``follow_symlinks`` -+should be set to ``True``:: - - web.static('/prefix', path_to_static_folder, follow_symlinks=True) - -+.. caution:: -+ -+ Enabling ``follow_symlinks`` can be a security risk, and may lead to -+ a directory transversal attack. You do NOT need this option to follow symlinks -+ which point to somewhere else within the static directory, this option is only -+ used to break out of the security sandbox. Enabling this option is highly -+ discouraged, and only expected to be used for edge cases in a local -+ development setting where remote users do not have access to the server. -+ - When you want to enable cache busting, - parameter ``append_version`` can be set to ``True`` - -diff --git a/docs/web_reference.rst b/docs/web_reference.rst -index 4073eb2..add37cd 100644 ---- a/docs/web_reference.rst -+++ b/docs/web_reference.rst -@@ -1802,9 +1802,15 @@ Router is any object that implements :class:`AbstractRouter` interface. - by default it's not allowed and HTTP/403 will - be returned on directory access. - -- :param bool follow_symlinks: flag for allowing to follow symlinks from -- a directory, by default it's not allowed and -- HTTP/404 will be returned on access. -+ :param bool follow_symlinks: flag for allowing to follow symlinks that lead -+ outside the static root directory, by default it's not allowed and -+ HTTP/404 will be returned on access. Enabling ``follow_symlinks`` -+ can be a security risk, and may lead to a directory transversal attack. -+ You do NOT need this option to follow symlinks which point to somewhere -+ else within the static directory, this option is only used to break out -+ of the security sandbox. Enabling this option is highly discouraged, -+ and only expected to be used for edge cases in a local development -+ setting where remote users do not have access to the server. - - :param bool append_version: flag for adding file version (hash) - to the url query string, this value will -diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py -index 0ba2e7c..e6269ef 100644 ---- a/tests/test_web_urldispatcher.py -+++ b/tests/test_web_urldispatcher.py -@@ -120,6 +120,97 @@ async def test_follow_symlink(tmp_dir_path, aiohttp_client) -> None: - assert (await r.text()) == data - - -+async def test_follow_symlink_directory_traversal( -+ tmp_path: pathlib.Path, aiohttp_client -+) -> None: -+ # Tests that follow_symlinks does not allow directory transversal -+ data = "private" -+ -+ private_file = tmp_path / "private_file" -+ private_file.write_text(data) -+ -+ safe_path = tmp_path / "safe_dir" -+ safe_path.mkdir() -+ -+ app = web.Application() -+ -+ # Register global static route: -+ app.router.add_static("/", str(safe_path), follow_symlinks=True) -+ client = await aiohttp_client(app) -+ -+ await client.start_server() -+ # We need to use a raw socket to test this, as the client will normalize -+ # the path before sending it to the server. -+ reader, writer = await asyncio.open_connection(client.host, client.port) -+ writer.write(b"GET /../private_file HTTP/1.1\r\n\r\n") -+ response = await reader.readuntil(b"\r\n\r\n") -+ assert b"404 Not Found" in response -+ writer.close() -+ await writer.wait_closed() -+ await client.close() -+ -+ -+async def test_follow_symlink_directory_traversal_after_normalization( -+ tmp_path: pathlib.Path, aiohttp_client -+) -> None: -+ # Tests that follow_symlinks does not allow directory transversal -+ # after normalization -+ # -+ # Directory structure -+ # |-- secret_dir -+ # | |-- private_file (should never be accessible) -+ # | |-- symlink_target_dir -+ # | |-- symlink_target_file (should be accessible via the my_symlink symlink) -+ # | |-- sandbox_dir -+ # | |-- my_symlink -> symlink_target_dir -+ # -+ secret_path = tmp_path / "secret_dir" -+ secret_path.mkdir() -+ -+ # This file is below the symlink target and should not be reachable -+ private_file = secret_path / "private_file" -+ private_file.write_text("private") -+ -+ symlink_target_path = secret_path / "symlink_target_dir" -+ symlink_target_path.mkdir() -+ -+ sandbox_path = symlink_target_path / "sandbox_dir" -+ sandbox_path.mkdir() -+ -+ # This file should be reachable via the symlink -+ symlink_target_file = symlink_target_path / "symlink_target_file" -+ symlink_target_file.write_text("readable") -+ -+ my_symlink_path = sandbox_path / "my_symlink" -+ pathlib.Path(str(my_symlink_path)).symlink_to(str(symlink_target_path), True) -+ -+ app = web.Application() -+ -+ # Register global static route: -+ app.router.add_static("/", str(sandbox_path), follow_symlinks=True) -+ client = await aiohttp_client(app) -+ -+ await client.start_server() -+ # We need to use a raw socket to test this, as the client will normalize -+ # the path before sending it to the server. -+ reader, writer = await asyncio.open_connection(client.host, client.port) -+ writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\n\r\n") -+ response = await reader.readuntil(b"\r\n\r\n") -+ assert b"404 Not Found" in response -+ writer.close() -+ await writer.wait_closed() -+ -+ reader, writer = await asyncio.open_connection(client.host, client.port) -+ writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\n\r\n") -+ response = await reader.readuntil(b"\r\n\r\n") -+ assert b"200 OK" in response -+ response = await reader.readuntil(b"readable") -+ assert response == b"readable" -+ writer.close() -+ await writer.wait_closed() -+ await client.close() -+ -+ - @pytest.mark.parametrize( - "dir_name,filename,data", - [ diff --git a/CVE-2024-23829.patch b/CVE-2024-23829.patch deleted file mode 100644 index 1a0d781d029313fd2c0fe9eb3f8b9b21fc3c9fab..0000000000000000000000000000000000000000 --- a/CVE-2024-23829.patch +++ /dev/null @@ -1,343 +0,0 @@ -From: Sam Bull -Date: Sun, 28 Jan 2024 17:09:58 +0000 -Subject: Improve validation in HTTP parser (#8074) (#8078) -MIME-Version: 1.0 -Content-Type: text/plain; charset="utf-8" -Content-Transfer-Encoding: 8bit - -Co-authored-by: Paul J. Dorn -Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) - -(cherry picked from commit 33ccdfb0a12690af5bb49bda2319ec0907fa7827) ---- - CONTRIBUTORS.txt | 1 + - aiohttp/http_parser.py | 28 +++++---- - tests/test_http_parser.py | 155 +++++++++++++++++++++++++++++++++++++++++++++- - 3 files changed, 169 insertions(+), 15 deletions(-) - -diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt -index ad63ce9..45fee0e 100644 ---- a/CONTRIBUTORS.txt -+++ b/CONTRIBUTORS.txt -@@ -215,6 +215,7 @@ Panagiotis Kolokotronis - Pankaj Pandey - Pau Freixes - Paul Colomiets -+Paul J. Dorn - Paulius Šileikis - Paulus Schoutsen - Pavel Kamaev -diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index 8e5e816..2d58947 100644 ---- a/aiohttp/http_parser.py -+++ b/aiohttp/http_parser.py -@@ -53,11 +53,10 @@ ASCIISET = set(string.printable) - # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / - # "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA - # token = 1*tchar --METHRE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") --VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d).(\d)") --HDRRE: Final[Pattern[bytes]] = re.compile( -- rb"[\x00-\x1F\x7F-\xFF()<>@,;:\[\]={} \t\"\\]" --) -+_TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~") -+TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+") -+VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) -+DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) - - RawRequestMessage = collections.namedtuple( - "RawRequestMessage", -@@ -122,6 +121,7 @@ class HeadersParser: - self, lines: List[bytes] - ) -> Tuple["CIMultiDictProxy[str]", RawHeaders]: - headers = CIMultiDict() # type: CIMultiDict[str] -+ # note: "raw" does not mean inclusion of OWS before/after the field value - raw_headers = [] - - lines_idx = 1 -@@ -135,13 +135,14 @@ class HeadersParser: - except ValueError: - raise InvalidHeader(line) from None - -+ if len(bname) == 0: -+ raise InvalidHeader(bname) -+ - # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 - if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"} - raise InvalidHeader(line) - - bvalue = bvalue.lstrip(b" \t") -- if HDRRE.search(bname): -- raise InvalidHeader(bname) - if len(bname) > self.max_field_size: - raise LineTooLong( - "request header name {}".format( -@@ -150,6 +151,9 @@ class HeadersParser: - str(self.max_field_size), - str(len(bname)), - ) -+ name = bname.decode("utf-8", "surrogateescape") -+ if not TOKENRE.fullmatch(name): -+ raise InvalidHeader(bname) - - header_length = len(bvalue) - -@@ -196,7 +200,6 @@ class HeadersParser: - ) - - bvalue = bvalue.strip(b" \t") -- name = bname.decode("utf-8", "surrogateescape") - value = bvalue.decode("utf-8", "surrogateescape") - - # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 -@@ -317,7 +320,8 @@ class HttpParser(abc.ABC): - if length is not None: - # Shouldn't allow +/- or other number formats. - # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 -- if not length.strip(" \t").isdigit(): -+ # msg.headers is already stripped of leading/trailing wsp -+ if not DIGITS.fullmatch(length): - raise InvalidHeader(CONTENT_LENGTH) - - length = int(length) -@@ -538,7 +542,7 @@ class HttpRequestParser(HttpParser): - path_part, _question_mark_separator, qs_part = path_part.partition("?") - - # method -- if not METHRE.match(method): -+ if not TOKENRE.fullmatch(method): - raise BadStatusLine(method) - - # version -@@ -615,8 +619,8 @@ class HttpResponseParser(HttpParser): - raise BadStatusLine(line) - version_o = HttpVersion(int(match.group(1)), int(match.group(2))) - -- # The status code is a three-digit number -- if len(status) != 3 or not status.isdigit(): -+ # The status code is a three-digit ASCII number, no padding -+ if len(status) != 3 or not DIGITS.fullmatch(status): - raise BadStatusLine(line) - status_i = int(status) - -diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index 9d65b2f..6073371 100644 ---- a/tests/test_http_parser.py -+++ b/tests/test_http_parser.py -@@ -3,7 +3,8 @@ - import asyncio - from unittest import mock - from urllib.parse import quote --from typing import Any -+from contextlib import nullcontext -+from typing import Any, Dict, List - - import pytest - from multidict import CIMultiDict -@@ -102,6 +103,20 @@ test2: data\r - assert not msg.upgrade - - -+def test_parse_unusual_request_line(parser) -> None: -+ if not isinstance(response, HttpResponseParserPy): -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ text = b"#smol //a HTTP/1.3\r\n\r\n" -+ messages, upgrade, tail = parser.feed_data(text) -+ assert len(messages) == 1 -+ msg, _ = messages[0] -+ assert msg.compression is None -+ assert not msg.upgrade -+ assert msg.method == "#smol" -+ assert msg.path == "//a" -+ assert msg.version == (1, 3) -+ -+ - def test_parse(parser) -> None: - text = b"GET /test HTTP/1.1\r\n\r\n" - messages, upgrade, tail = parser.feed_data(text) -@@ -365,6 +380,51 @@ def test_headers_content_length_err_2(parser) -> None: - parser.feed_data(text) - - -+_pad: Dict[bytes, str] = { -+ b"": "empty", -+ # not a typo. Python likes triple zero -+ b"\000": "NUL", -+ b" ": "SP", -+ b" ": "SPSP", -+ # not a typo: both 0xa0 and 0x0a in case of 8-bit fun -+ b"\n": "LF", -+ b"\xa0": "NBSP", -+ b"\t ": "TABSP", -+} -+ -+ -+@pytest.mark.parametrize("hdr", [b"", b"foo"], ids=["name-empty", "with-name"]) -+@pytest.mark.parametrize("pad2", _pad.keys(), ids=["post-" + n for n in _pad.values()]) -+@pytest.mark.parametrize("pad1", _pad.keys(), ids=["pre-" + n for n in _pad.values()]) -+def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) -> None: -+ text = b"GET /test HTTP/1.1\r\n" b"%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2) -+ if isinstance(parser, HttpRequestParserPy): -+ expectation = pytest.raises(http_exceptions.InvalidHeader) -+ else: -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ if pad1 == pad2 == b"" and hdr != b"": -+ # one entry in param matrix is correct: non-empty name, not padded -+ expectation = nullcontext() -+ if pad1 == pad2 == hdr == b"": -+ if not isinstance(response, HttpResponseParserPy): -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ # work around pytest.raises not working -+ try: -+ parser.feed_data(text) -+ except aiohttp.http_exceptions.InvalidHeader: -+ pass -+ except aiohttp.http_exceptions.BadHttpMessage: -+ pass -+ -+ -+def test_empty_header_name(parser) -> None: -+ if not isinstance(response, HttpResponseParserPy): -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ text = b"GET /test HTTP/1.1\r\n" b":test\r\n\r\n" -+ with pytest.raises(http_exceptions.BadHttpMessage): -+ parser.feed_data(text) -+ -+ - def test_invalid_header(parser) -> None: - text = b"GET /test HTTP/1.1\r\n" b"test line\r\n\r\n" - with pytest.raises(http_exceptions.BadHttpMessage): -@@ -387,11 +447,27 @@ def test_cve_2023_37276(parser: Any) -> None: - pytest.xfail("Regression test for Py parser. May match C behaviour later.") - - -+@pytest.mark.parametrize( -+ "rfc9110_5_6_2_token_delim", -+ r'"(),/:;<=>?@[\]{}', -+) -+def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None: -+ text = f"POST / HTTP/1.1\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode() -+ expectation = pytest.raises(http_exceptions.BadHttpMessage) -+ if rfc9110_5_6_2_token_delim == ":": -+ # Inserting colon into header just splits name/value earlier. -+ expectation = nullcontext() -+ with expectation: -+ parser.feed_data(text) -+ -+ - @pytest.mark.parametrize( - "hdr", - ( - "Content-Length: -5", # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length - "Content-Length: +256", -+ "Content-Length: \N{superscript one}", -+ "Content-Length: \N{mathematical double-struck digit one}", - "Foo: abc\rdef", # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 - "Bar: abc\ndef", - "Baz: abc\x00def", -@@ -563,6 +639,42 @@ def test_http_request_bad_status_line(parser) -> None: - parser.feed_data(text) - - -+_num: Dict[bytes, str] = { -+ # dangerous: accepted by Python int() -+ # unicodedata.category("\U0001D7D9") == 'Nd' -+ "\N{mathematical double-struck digit one}".encode(): "utf8digit", -+ # only added for interop tests, refused by Python int() -+ # unicodedata.category("\U000000B9") == 'No' -+ "\N{superscript one}".encode(): "utf8number", -+ "\N{superscript one}".encode("latin-1"): "latin1number", -+} -+ -+ -+@pytest.mark.parametrize("nonascii_digit", _num.keys(), ids=_num.values()) -+def test_http_request_bad_status_line_number( -+ parser: Any, nonascii_digit: bytes -+) -> None: -+ text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\n\r\n" -+ if isinstance(parser, HttpRequestParserPy): -+ exception = http_exceptions.BadStatusLine -+ else: -+ exception = http_exceptions.BadHttpMessage -+ with pytest.raises(exception): -+ parser.feed_data(text) -+ -+ -+def test_http_request_bad_status_line_separator(parser: Any) -> None: -+ # single code point, old, multibyte NFKC, multibyte NFKD -+ utf8sep = "\N{arabic ligature sallallahou alayhe wasallam}".encode() -+ text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\n\r\n" -+ if isinstance(parser, HttpRequestParserPy): -+ exception = http_exceptions.BadStatusLine -+ else: -+ exception = http_exceptions.BadHttpMessage -+ with pytest.raises(exception): -+ parser.feed_data(text) -+ -+ - def test_http_request_bad_status_line_whitespace(parser: Any) -> None: - text = b"GET\n/path\fHTTP/1.1\r\n\r\n" - with pytest.raises(http_exceptions.BadStatusLine): -@@ -584,6 +696,31 @@ def test_http_request_upgrade(parser: Any) -> None: - assert tail == b"some raw data" - - -+def test_http_request_parser_utf8_request_line(parser) -> None: -+ if not isinstance(response, HttpResponseParserPy): -+ pytest.xfail("Regression test for Py parser. May match C behaviour later.") -+ messages, upgrade, tail = parser.feed_data( -+ # note the truncated unicode sequence -+ b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\n" + -+ # for easier grep: ASCII 0xA0 more commonly known as non-breaking space -+ # note the leading and trailing spaces -+ "sTeP: \N{latin small letter sharp s}nek\t\N{no-break space} " -+ "\r\n\r\n".encode() -+ ) -+ msg = messages[0][0] -+ -+ assert msg.method == "GET" -+ assert msg.path == "/Pünktchen\udca0\udcef\udcb7" -+ assert msg.version == (1, 1) -+ assert msg.headers == CIMultiDict([("STEP", "ßnek\t\xa0")]) -+ assert msg.raw_headers == ((b"sTeP", "ßnek\t\xa0".encode()),) -+ assert not msg.should_close -+ assert msg.compression is None -+ assert not msg.upgrade -+ assert not msg.chunked -+ assert msg.url.path == URL("/P%C3%BCnktchen\udca0\udcef\udcb7").path -+ -+ - def test_http_request_parser_utf8(parser) -> None: - text = "GET /path HTTP/1.1\r\nx-test:тест\r\n\r\n".encode() - messages, upgrade, tail = parser.feed_data(text) -@@ -633,9 +770,15 @@ def test_http_request_parser_two_slashes(parser) -> None: - assert not msg.chunked - - --def test_http_request_parser_bad_method(parser) -> None: -+@pytest.mark.parametrize( -+ "rfc9110_5_6_2_token_delim", -+ [bytes([i]) for i in rb'"(),/:;<=>?@[\]{}'], -+) -+def test_http_request_parser_bad_method( -+ parser, rfc9110_5_6_2_token_delim: bytes -+) -> None: - with pytest.raises(http_exceptions.BadStatusLine): -- parser.feed_data(b'=":(e),[T];?" /get HTTP/1.1\r\n\r\n') -+ parser.feed_data(rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\n\r\n') - - - def test_http_request_parser_bad_version(parser) -> None: -@@ -751,6 +894,12 @@ def test_http_response_parser_code_not_int(response) -> None: - response.feed_data(b"HTTP/1.1 ttt test\r\n\r\n") - - -+@pytest.mark.parametrize("nonascii_digit", _num.keys(), ids=_num.values()) -+def test_http_response_parser_code_not_ascii(response, nonascii_digit: bytes) -> None: -+ with pytest.raises(http_exceptions.BadStatusLine): -+ response.feed_data(b"HTTP/1.1 20" + nonascii_digit + b" test\r\n\r\n") -+ -+ - def test_http_request_chunked_payload(parser) -> None: - text = b"GET /test HTTP/1.1\r\n" b"transfer-encoding: chunked\r\n\r\n" - msg, payload = parser.feed_data(text)[0][0] diff --git a/CVE-2024-27306.patch b/CVE-2024-27306.patch index 52b007e05df243bcefc272e5f3f1b617d195ff38..6449b2c25ef202b64ea73840d47a8792e0984173 100644 --- a/CVE-2024-27306.patch +++ b/CVE-2024-27306.patch @@ -1,17 +1,27 @@ +From 28335525d1eac015a7e7584137678cbb6ff19397 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Thu, 11 Apr 2024 15:54:45 +0100 -Subject: Escape filenames and paths in HTML when generating index pages - (#8317) (#8319) +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 | 125 +++++++++++++++++++++++++++++++++++----- - 2 files changed, 117 insertions(+), 20 deletions(-) + 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 48557d5..c6fd34d 100644 +index 9969653344..954291f644 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -1,7 +1,9 @@ @@ -24,16 +34,16 @@ index 48557d5..c6fd34d 100644 import inspect import keyword import os -@@ -85,6 +87,8 @@ _WebHandler = Callable[[Request], Awaitable[StreamResponse]] - _ExpectHandler = Callable[[Request], Awaitable[None]] - _Resolve = Tuple[Optional[AbstractMatchInfo], Set[str]] +@@ -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 -@@ -702,7 +706,7 @@ class StaticResource(PrefixResource): +@@ -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() @@ -42,7 +52,7 @@ index 48557d5..c6fd34d 100644 h1 = f"

{index_of}

" index_list = [] -@@ -710,7 +714,7 @@ class StaticResource(PrefixResource): +@@ -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() @@ -51,7 +61,7 @@ index 48557d5..c6fd34d 100644 # if file is a directory, add '/' to the end of the name if _file.is_dir(): -@@ -719,9 +723,7 @@ class StaticResource(PrefixResource): +@@ -725,9 +729,7 @@ def _directory_as_html(self, filepath: Path) -> str: file_name = _file.name index_list.append( @@ -63,18 +73,18 @@ index 48557d5..c6fd34d 100644 ul = "
    \n{}\n
".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 e6269ef..c46c76c 100644 +index 76e533e473..0441890c10 100644 --- a/tests/test_web_urldispatcher.py +++ b/tests/test_web_urldispatcher.py -@@ -7,6 +7,7 @@ import sys - import tempfile +@@ -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 -+from typing import Optional - - import pytest - -@@ -32,35 +33,42 @@ def tmp_dir_path(request): +@@ -14,31 +15,38 @@ @pytest.mark.parametrize( @@ -126,16 +136,19 @@ index e6269ef..c46c76c 100644 ], ) async def test_access_root_of_static_handler( -- tmp_dir_path, aiohttp_client, show_index, status, prefix, data -+ tmp_dir_path, aiohttp_client, show_index, status, prefix, request_path, data +@@ -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. - # Try to access the root of static file server, and make -@@ -85,7 +93,94 @@ async def test_access_root_of_static_handler( +@@ -72,7 +81,94 @@ async def test_access_root_of_static_handler( client = await aiohttp_client(app) # Request the root of the static directory. -- r = await client.get(prefix) +- async with await client.get(prefix) as r: + async with await client.get(request_path) as r: + assert r.status == status + @@ -192,7 +205,7 @@ index e6269ef..c46c76c 100644 +) +async def test_access_root_of_static_handler_xss( + tmp_path: pathlib.Path, -+ aiohttp_client, ++ aiohttp_client: AiohttpClient, + show_index: bool, + status: int, + prefix: str, @@ -223,7 +236,7 @@ index e6269ef..c46c76c 100644 + client = await aiohttp_client(app) + + # Request the root of the static directory. -+ r = await client.get(request_path) - assert r.status == status ++ async with await client.get(request_path) as r: + assert r.status == status - if data: + if data: diff --git a/CVE-2024-30251-Followup-01.patch b/CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch similarity index 70% rename from CVE-2024-30251-Followup-01.patch rename to CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch index 4f2b745b2bdc859827dd0aa06bb7fd48721a704b..2dcce4d4a0988fd9a7c5a859efcc9dea352bb32e 100644 --- a/CVE-2024-30251-Followup-01.patch +++ b/CVE-2024-30251-PR-8332-482e6cdf-backport-3.9-Add-set_content_dispos.patch @@ -1,21 +1,32 @@ +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: Add set_content_disposition test (#8333) +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 --- + CHANGES/8332.bugfix.rst | 1 + aiohttp/multipart.py | 7 +++++-- tests/test_multipart.py | 7 +++++++ - 2 files changed, 12 insertions(+), 2 deletions(-) + 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 f2c4ead..ac7dfdb 100644 +index a43ec54571..fcdf16183c 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py -@@ -841,8 +841,6 @@ class MultipartWriter(Payload): +@@ -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 @@ -24,7 +35,7 @@ index f2c4ead..ac7dfdb 100644 assert ( not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} & payload.headers.keys() -@@ -923,6 +921,11 @@ class MultipartWriter(Payload): +@@ -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: @@ -37,10 +48,10 @@ index f2c4ead..ac7dfdb 100644 await writer.write(part._binary_headers) diff --git a/tests/test_multipart.py b/tests/test_multipart.py -index e17817d..89db7f8 100644 +index dbfaf74b9b..37ac54797f 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py -@@ -1122,6 +1122,13 @@ class TestMultipartWriter: +@@ -1282,6 +1282,13 @@ def test_append_multipart(self, writer) -> None: part = writer._parts[0][0] assert part.headers[CONTENT_TYPE] == "test/passed" diff --git a/CVE-2024-30251-Followup-02.patch b/CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch similarity index 75% rename from CVE-2024-30251-Followup-02.patch rename to CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch index 84a5a99ccdf02ae7f5a974308f3d835700b999df..f302e2a0b146aee9fc93a276874aeda793669fe6 100644 --- a/CVE-2024-30251-Followup-02.patch +++ b/CVE-2024-30251-PR-8335-5a6949da-backport-3.9-Add-Content-Dispositio.patch @@ -1,21 +1,32 @@ +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: Add Content-Disposition automatically (#8336) +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 --- + CHANGES/8335.bugfix.rst | 1 + aiohttp/multipart.py | 4 ++++ tests/test_multipart.py | 22 +++++++++++++++++----- - 2 files changed, 21 insertions(+), 5 deletions(-) + 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 ac7dfdb..ac7a459 100644 +index fcdf16183c..71fc2654a1 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py -@@ -845,6 +845,10 @@ class MultipartWriter(Payload): +@@ -852,6 +852,10 @@ def append_payload(self, payload: Payload) -> Payload: not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} & payload.headers.keys() ) @@ -27,10 +38,10 @@ index ac7dfdb..ac7a459 100644 # compression encoding = payload.headers.get(CONTENT_ENCODING, "").lower() diff --git a/tests/test_multipart.py b/tests/test_multipart.py -index 89db7f8..cff9c08 100644 +index 37ac54797f..436b70957f 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py -@@ -1122,12 +1122,24 @@ class TestMultipartWriter: +@@ -1282,12 +1282,24 @@ def test_append_multipart(self, writer) -> None: part = writer._parts[0][0] assert part.headers[CONTENT_TYPE] == "test/passed" diff --git a/CVE-2024-30251.patch b/CVE-2024-30251.patch index 7fb05637d9fce8b8e9cbdb9df27e6b4f54a1db76..828022dbfe4e5cfd67881d83f5f2e16def8c27fd 100644 --- a/CVE-2024-30251.patch +++ b/CVE-2024-30251.patch @@ -1,34 +1,78 @@ +From cebe526b9c34dc3a3da9140409db63014bc4cf19 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 7 Apr 2024 13:19:31 +0100 -Subject: Fix handling of multipart/form-data (#8280) (#8302) +Subject: [PATCH] Fix handling of multipart/form-data (#8280) (#8302) https://datatracker.ietf.org/doc/html/rfc7578 (cherry picked from commit 7d0be3fee540a3d4161ac7dc76422f1f5ea60104) --- - aiohttp/formdata.py | 1 - - aiohttp/multipart.py | 121 ++++++++++++++++++++++++++-------------- - tests/test_client_functional.py | 44 +-------------- - tests/test_multipart.py | 68 +++++++++++++++++----- - tests/test_web_functional.py | 2 +- - 5 files changed, 137 insertions(+), 99 deletions(-) + 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() ++` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py -index 900716b..f160e5c 100644 +index e7cd24ca9f7..2b75b3de72c 100644 --- a/aiohttp/formdata.py +++ b/aiohttp/formdata.py -@@ -79,7 +79,6 @@ class FormData: +@@ -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 9e1ca92..f2c4ead 100644 +index 4471dd4bb7e..a43ec545713 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py -@@ -251,13 +251,22 @@ class BodyPartReader: +@@ -256,13 +256,22 @@ class BodyPartReader: chunk_size = 8192 def __init__( @@ -52,8 +96,8 @@ index 9e1ca92..f2c4ead 100644 + 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 - # TODO: typeing.Deque is not supported by Python 3.5 -@@ -325,6 +334,8 @@ class BodyPartReader: + 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) @@ -62,7 +106,7 @@ index 9e1ca92..f2c4ead 100644 return chunk async def _read_chunk_from_stream(self, size: int) -> bytes: -@@ -440,7 +451,8 @@ class BodyPartReader: +@@ -449,7 +460,8 @@ def decode(self, data: bytes) -> bytes: """ if CONTENT_TRANSFER_ENCODING in self.headers: data = self._decode_content_transfer(data) @@ -72,7 +116,7 @@ index 9e1ca92..f2c4ead 100644 return self._decode_content(data) return data -@@ -474,7 +486,7 @@ class BodyPartReader: +@@ -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) @@ -81,7 +125,7 @@ index 9e1ca92..f2c4ead 100644 @reify def name(self) -> Optional[str]: -@@ -528,9 +540,17 @@ class MultipartReader: +@@ -538,9 +550,17 @@ class MultipartReader: part_reader_cls = BodyPartReader def __init__(self, headers: Mapping[str, str], content: StreamReader) -> None: @@ -96,10 +140,10 @@ index 9e1ca92..f2c4ead 100644 self._boundary = ("--" + self._get_boundary()).encode() self._content = content + self._default_charset: Optional[str] = None - self._last_part = ( - None - ) # type: Optional[Union['MultipartReader', BodyPartReader]] -@@ -586,7 +606,24 @@ class MultipartReader: + 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 @@ -125,7 +169,7 @@ index 9e1ca92..f2c4ead 100644 return self._last_part async def release(self) -> None: -@@ -621,19 +658,16 @@ class MultipartReader: +@@ -628,19 +665,16 @@ def _get_part_reader( return type(self)(headers, self._content) return self.multipart_reader_cls(headers, self._content) else: @@ -153,33 +197,33 @@ index 9e1ca92..f2c4ead 100644 if len(boundary) > 70: raise ValueError("boundary %r is too long (70 chars max)" % boundary) -@@ -724,6 +758,7 @@ class MultipartWriter(Payload): +@@ -731,6 +765,7 @@ def __init__(self, subtype: str = "mixed", boundary: Optional[str] = None) -> No super().__init__(None, content_type=ctype) - self._parts = [] # type: List[_Part] + self._parts: List[_Part] = [] + self._is_form_data = subtype == "form-data" def __enter__(self) -> "MultipartWriter": return self -@@ -801,32 +836,36 @@ class MultipartWriter(Payload): +@@ -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 = payload.headers.get( +- encoding: Optional[str] = payload.headers.get( - CONTENT_ENCODING, - "", -- ).lower() # type: Optional[str] +- ).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( +- te_encoding: Optional[str] = payload.headers.get( - CONTENT_TRANSFER_ENCODING, - "", -- ).lower() # type: Optional[str] +- ).lower() - if te_encoding not in ("", "base64", "quoted-printable", "binary"): - raise RuntimeError( - "unknown content transfer encoding: {}" "".format(te_encoding) @@ -221,13 +265,13 @@ index 9e1ca92..f2c4ead 100644 + 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 + 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 bd83098..5dc0fdf 100644 +index 8a9a4e184be..dbb2dff5ac4 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py -@@ -1154,48 +1154,6 @@ async def test_POST_DATA_with_charset_post(aiohttp_client) -> None: +@@ -1317,48 +1317,6 @@ async def handler(request): resp.close() @@ -276,25 +320,25 @@ index bd83098..5dc0fdf 100644 async def test_POST_MultiDict(aiohttp_client) -> None: async def handler(request): data = await request.post() -@@ -1243,7 +1201,7 @@ async def test_POST_FILES(aiohttp_client, fname) -> None: - client = await aiohttp_client(app) +@@ -1410,7 +1368,7 @@ async def handler(request): with fname.open("rb") as f: -- resp = await client.post("/", data={"some": f, "test": b"data"}, chunked=True) -+ resp = await client.post("/", data={"some": f, "test": io.BytesIO(b"data")}, chunked=True) - assert 200 == resp.status - resp.close() + 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 6c3f121..e17817d 100644 +index f9d130e7949..dbfaf74b9b7 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py -@@ -784,6 +784,58 @@ class TestMultipartReader: - assert first.at_eof() - assert not second.at_eof() +@@ -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: -+ stream = Stream( ++ with Stream( + b"--:\r\n" + b'Content-Disposition: form-data; name="_charset_"\r\n\r\n' + b"ascii" @@ -312,23 +356,23 @@ index 6c3f121..e17817d 100644 + b'Content-Disposition: form-data; name="field3"\r\n\r\n' + b"foo" + b"\r\n" -+ ) -+ 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" ++ ) 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: -+ stream = Stream( ++ 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" @@ -337,18 +381,18 @@ index 6c3f121..e17817d 100644 + b'Content-Disposition: form-data; name="field1"\r\n\r\n' + b"foo" + b"\r\n" -+ ) -+ reader = aiohttp.MultipartReader( -+ {CONTENT_TYPE: 'multipart/form-data;boundary=":"'}, -+ stream, -+ ) -+ with pytest.raises(RuntimeError, match="Invalid default charset"): -+ await reader.next() ++ ) 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 -@@ -1120,7 +1172,6 @@ class TestMultipartWriter: +@@ -1280,7 +1332,6 @@ async def test_preserve_content_disposition_header(self, buf, stream): CONTENT_TYPE: "text/python", }, ) @@ -356,7 +400,7 @@ index 6c3f121..e17817d 100644 await writer.write(stream) assert part.headers[CONTENT_TYPE] == "text/python" -@@ -1131,9 +1182,7 @@ class TestMultipartWriter: +@@ -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" @@ -367,7 +411,7 @@ index 6c3f121..e17817d 100644 ) async def test_set_content_disposition_override(self, buf, stream): -@@ -1147,7 +1196,6 @@ class TestMultipartWriter: +@@ -1307,7 +1356,6 @@ async def test_set_content_disposition_override(self, buf, stream): CONTENT_TYPE: "text/python", }, ) @@ -375,7 +419,7 @@ index 6c3f121..e17817d 100644 await writer.write(stream) assert part.headers[CONTENT_TYPE] == "text/python" -@@ -1158,9 +1206,7 @@ class TestMultipartWriter: +@@ -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" @@ -386,7 +430,7 @@ index 6c3f121..e17817d 100644 ) async def test_reset_content_disposition_header(self, buf, stream): -@@ -1172,8 +1218,6 @@ class TestMultipartWriter: +@@ -1332,8 +1378,6 @@ async def test_reset_content_disposition_header(self, buf, stream): headers={CONTENT_TYPE: "text/plain"}, ) @@ -395,22 +439,68 @@ index 6c3f121..e17817d 100644 assert CONTENT_DISPOSITION in part.headers part.set_content_disposition("attachments", filename="bug.py") -@@ -1186,9 +1230,7 @@ class TestMultipartWriter: +@@ -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\"; filename*=utf-8''bug.py\r\n" +- b' attachments; filename="bug.py"\r\n' - b"Content-Length: %s" - b"" % (str(content_length).encode(),) -+ b" attachments; filename=\"bug.py\"; filename*=utf-8''bug.py" ++ b' attachments; filename="bug.py"' ) diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py -index a28fcd4..3541629 100644 +index 04fc2e35fd1..ee61537068b 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py -@@ -634,7 +634,7 @@ async def test_upload_file(aiohttp_client) -> None: +@@ -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) @@ -418,4 +508,4 @@ index a28fcd4..3541629 100644 + 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 new file mode 100644 index 0000000000000000000000000000000000000000..2e1b42696697ef259c9a312c0e42c565dda87435 --- /dev/null +++ b/CVE-2024-42367.patch @@ -0,0 +1,198 @@ +From f98240ad2279c3e97b65eddce40d37948f383416 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +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 +(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/CVE-2024-52304.patch b/CVE-2024-52304.patch index 6cbb192135ca9ca5f48534ff8dd39c3371e12f50..b0a3b2e4832cf1ea1058d000d8543f2c74cc441a 100644 --- a/CVE-2024-52304.patch +++ b/CVE-2024-52304.patch @@ -1,14 +1,13 @@ -From 541d86d9e7884590c655876cd40042565293d8df Mon Sep 17 00:00:00 2001 +From 259edc369075de63e6f3a4eaade058c62af0df71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" -Date: Wed, 13 Nov 2024 08:14:06 -0600 -Subject: [PATCH] Fix incorrect parsing of chunk extensions with the pure - Python parser (#9851) - +Date: Wed, 13 Nov 2024 08:50:36 -0600 +Subject: [PATCH] [PR #9851/541d86d backport][3.10] Fix incorrect parsing of + chunk extensions with the pure Python parser (#9853) --- CHANGES/9851.bugfix.rst | 1 + aiohttp/http_parser.py | 7 ++++++ - tests/test_http_parser.py | 48 ++++++++++++++++++++++++++++++++++++++- - 3 files changed, 55 insertions(+), 1 deletion(-) + tests/test_http_parser.py | 51 ++++++++++++++++++++++++++++++++++++++- + 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 CHANGES/9851.bugfix.rst diff --git a/CHANGES/9851.bugfix.rst b/CHANGES/9851.bugfix.rst @@ -19,10 +18,10 @@ index 0000000..02541a9 @@ -0,0 +1 @@ +Fixed incorrect parsing of chunk extensions with the pure Python parser -- by :user:`bdraco`. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py -index b182877..af42272 100644 +index d7b8dac..deee4f5 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py -@@ -730,6 +730,13 @@ class HttpPayloadParser: +@@ -833,6 +833,13 @@ class HttpPayloadParser: i = chunk.find(CHUNK_EXT, 0, pos) if i >= 0: size_b = chunk[:i] # strip chunk-extensions @@ -30,28 +29,29 @@ index b182877..af42272 100644 + if b"\n" in (ext := chunk[i:pos]): + exc = BadHttpMessage( + f"Unexpected LF in chunk-extension: {ext!r}" -+ ) ++ ) + set_exception(self.payload, exc) + raise exc else: size_b = chunk[:pos] diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py -index 9db34dd..6e14827 100644 +index 0417fa4..d348bae 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py -@@ -10,6 +10,7 @@ from yarl import URL +@@ -13,6 +13,7 @@ from yarl import URL import aiohttp from aiohttp import http_exceptions, streams +from aiohttp.base_protocol import BaseProtocol from aiohttp.http_parser import ( + NO_EXTENSIONS, DeflateBuffer, - HttpPayloadParser, -@@ -758,8 +759,53 @@ def test_parse_no_length_payload(parser) -> None: - msg, payload = parser.feed_data(text)[0][0] - assert payload.is_eof() +@@ -1337,7 +1338,55 @@ def test_parse_chunked_payload_empty_body_than_another_chunked( + assert b"second" == b"".join(d for d in payload._buffer) + +-def test_partial_url(parser: Any) -> None: +@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.") +async def test_parse_chunked_payload_with_lf_in_extensions_c_parser( + loop: asyncio.AbstractEventLoop, protocol: BaseProtocol @@ -73,6 +73,8 @@ index 9db34dd..6e14827 100644 + ) + with pytest.raises(http_exceptions.BadHttpMessage, match="\\\\nxx"): + parser.feed_data(payload) ++ ++ +async def test_parse_chunked_payload_with_lf_in_extensions_py_parser( + loop: asyncio.AbstractEventLoop, protocol: BaseProtocol +) -> None: @@ -97,12 +99,11 @@ index 9db34dd..6e14827 100644 + assert isinstance(reader.exception(), http_exceptions.BadHttpMessage) + assert "\\nxx" in str(reader.exception()) + - --def test_partial_url(parser) -> None: ++ +def test_partial_url(parser: HttpRequestParser) -> None: messages, upgrade, tail = parser.feed_data(b"GET /te") assert len(messages) == 0 messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\n\r\n") -- -2.43.0 +2.41.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 new file mode 100644 index 0000000000000000000000000000000000000000..1e8bd2bc06eb8a3756eefa9af4aace6764144ea8 --- /dev/null +++ b/Fix-Python-parser-to-mark-responses-without-length-a.patch @@ -0,0 +1,57 @@ +From 3223e1209285d96cfe5ac92c68653c5690e6e721 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=E8=82=96=E5=9C=A8?= +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.7.4.post0.tar.gz b/aiohttp-3.7.4.post0.tar.gz deleted file mode 100644 index bb0fc48df313e11c4f85285aef0a09812eb46158..0000000000000000000000000000000000000000 Binary files a/aiohttp-3.7.4.post0.tar.gz and /dev/null differ diff --git a/aiohttp-3.9.3.tar.gz b/aiohttp-3.9.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..863954bb448419a5eaaa849431290b5e9249f95a Binary files /dev/null and b/aiohttp-3.9.3.tar.gz differ diff --git a/change-require-chardet-package-version.patch b/change-require-chardet-package-version.patch deleted file mode 100644 index 8441513647a265bda243d617e0570e77a384f215..0000000000000000000000000000000000000000 --- a/change-require-chardet-package-version.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff -Nur a/aiohttp.egg-info/requires.txt b/aiohttp.egg-info/requires.txt ---- a/aiohttp.egg-info/requires.txt 2021-03-07 04:59:20.000000000 +0800 -+++ b/aiohttp.egg-info/requires.txt 2022-11-07 15:55:07.214419948 +0800 -@@ -1,5 +1,5 @@ - attrs>=17.3.0 --chardet<5.0,>=2.0 -+chardet<=5.0,>=2.0 - multidict<7.0,>=4.5 - async_timeout<4.0,>=3.0 - yarl<2.0,>=1.0 -diff -Nur a/setup.py b/setup.py ---- a/setup.py 2021-03-07 04:59:13.000000000 +0800 -+++ b/setup.py 2022-11-07 15:55:46.671059857 +0800 -@@ -66,7 +66,7 @@ - - install_requires = [ - "attrs>=17.3.0", -- "chardet>=2.0,<5.0", -+ "chardet>=2.0,<=5.0", - "multidict>=4.5,<7.0", - "async_timeout>=3.0,<4.0", - "yarl>=1.0,<2.0", diff --git a/python-aiohttp.spec b/python-aiohttp.spec index f8f5671ef1991c41ff7c2ac7e94edfd5c0f697c6..55b86dce5601b230095501f78ac6fd896285d2b6 100644 --- a/python-aiohttp.spec +++ b/python-aiohttp.spec @@ -1,31 +1,37 @@ %global _empty_manifest_terminate_build 0 -Name: python-aiohttp -Version: 3.7.4 -Release: 6 -Summary: Async http client/server framework (asyncio) -License: Apache 2 -URL: https://github.com/aio-libs/aiohttp -Source0: https://files.pythonhosted.org/packages/99/f5/90ede947a3ce2d6de1614799f5fea4e93c19b6520a59dc5d2f64123b032f/aiohttp-3.7.4.post0.tar.gz -Patch0: change-require-chardet-package-version.patch -Patch1: CVE-2023-47641.patch -Patch2: CVE-2023-49081.patch -Patch3: CVE-2024-52304.patch -Patch4: CVE-2023-47627.patch -Patch5: CVE-2023-49082.patch -Patch6: CVE-2024-23334.patch -Patch7: CVE-2024-23829.patch -Patch8: CVE-2024-27306.patch -Patch9: CVE-2024-30251.patch -Patch10: CVE-2024-30251-Followup-01.patch -Patch11: CVE-2024-30251-Followup-02.patch - -BuildRequires: python3-attrs -BuildRequires: python3-chardet -BuildRequires: python3-multidict -BuildRequires: python3-async-timeout -BuildRequires: python3-yarl -BuildRequires: python3-typing-extensions -BuildRequires: python3-cchardet +Name: python-aiohttp +Version: 3.9.3 +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 +#https://github.com/aio-libs/aiohttp/commit/259edc369075de63e6f3a4eaade058c62af0df71.patch +Patch6: CVE-2024-52304.patch + +Requires: python3-attrs +Requires: python3-charset-normalizer +Requires: python3-multidict +Requires: python3-async-timeout +Requires: python3-yarl +Requires: python3-frozenlist +Requires: python3-aiosignal +Requires: python3-asynctest +Requires: python3-typing-extensions +Requires: python3-aiodns +Requires: python3-Brotli +Requires: python3-cchardet %description Async http client/server framework (asyncio). @@ -48,9 +54,20 @@ Provides: python3-aiohttp-doc Development documents and examples for aiohttp. %prep -%autosetup -n aiohttp-3.7.4.post0 -p1 +%autosetup -n aiohttp-%{version} -p1 %build +sed -i 's|async-timeout >= 4.0, < 5.0 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' setup.cfg +sed -i 's|async-timeout==4.0.3 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/runtime-deps.txt +sed -i 's|async-timeout==4.0.3 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/dev.txt +sed -i 's|async-timeout==4.0.3 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/constraints.txt +sed -i 's|async-timeout >= 4.0, < 5.0 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/runtime-deps.in +sed -i 's|async-timeout==4.0.3 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/base.txt +sed -i 's|async-timeout==4.0.3 ; python_version < "3.11"|async-timeout; python_version < "3.11"|' requirements/test.txt +sed -i 's|async-timeout==4.0.3|async-timeout|' requirements/lint.txt +sed -i 's|async-timeout<5.0,>=4.0|async-timeout|' aiohttp.egg-info/requires.txt +sed -i 's|Requires-Dist: async-timeout<5.0,>=4.0; python_version < "3.11"|Requires-Dist: async-timeout; python_version < "3.11"|' aiohttp.egg-info/PKG-INFO +sed -i 's|Requires-Dist: async-timeout<5.0,>=4.0; python_version < "3.11"|Requires-Dist: async-timeout; python_version < "3.11"|' PKG-INFO %py3_build %install @@ -88,6 +105,9 @@ mv %{buildroot}/doclist.lst . %{_docdir}/* %changelog +* Mon Apr 7 2025 fuanan - 3.9.3-1 +- Update version to 3.9.3 + * Thu Mar 06 2025 yaoxin <1024769339@qq.com> - 3.7.4-6 - Fix CVE-2023-47627,CVE-2023-49082,CVE-2024-23334,CVE-2024-23829,CVE-2024-27306 and CVE-2024-30251 diff --git a/python-aiohttp.yaml b/python-aiohttp.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bf8d20fc4104584d07bb35ee1dea065e931fa495 --- /dev/null +++ b/python-aiohttp.yaml @@ -0,0 +1,4 @@ +version_control: github +src_repo: aio-libs/aiohttp +tag_prefix: ^v +separator: .