Version in base suite: 3.8.4-1 Base version: python-aiohttp_3.8.4-1 Target version: python-aiohttp_3.8.4-1+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-aiohttp/python-aiohttp_3.8.4-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-aiohttp/python-aiohttp_3.8.4-1+deb12u1.dsc changelog | 11 patches/CVE-2023-47627.patch | 314 ++++++++++++++++++++++++ patches/CVE-2023-49081.patch | 70 +++++ patches/CVE-2023-49082.patch | 53 ++++ patches/CVE-2024-23334-2.patch | 12 patches/CVE-2024-23334.patch | 190 ++++++++++++++ patches/CVE-2024-30251.patch | 526 +++++++++++++++++++++++++++++++++++++++++ patches/CVE-2024-52304.patch | 86 ++++++ patches/series | 7 9 files changed, 1269 insertions(+) diff -Nru python-aiohttp-3.8.4/debian/changelog python-aiohttp-3.8.4/debian/changelog --- python-aiohttp-3.8.4/debian/changelog 2023-02-16 10:54:52.000000000 +0000 +++ python-aiohttp-3.8.4/debian/changelog 2024-11-28 19:38:32.000000000 +0000 @@ -1,3 +1,14 @@ +python-aiohttp (3.8.4-1+deb12u1) bookworm-security; urgency=medium + + * CVE-2023-47627 + * CVE-2023-49081 (Closes: #1057163) + * CVE-2023-49082 (Closes: #1057164) + * CVE-2024-23334 (Closes: #1062709) + * CVE-2024-30251 (Closes: #1070364) + * CVE-2024-52304 (Closes: #1088109) + + -- Moritz Mühlenhoff Thu, 28 Nov 2024 20:38:32 +0100 + python-aiohttp (3.8.4-1) unstable; urgency=medium [ Debian Janitor ] diff -Nru python-aiohttp-3.8.4/debian/patches/CVE-2023-47627.patch python-aiohttp-3.8.4/debian/patches/CVE-2023-47627.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2023-47627.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2023-47627.patch 2024-11-24 19:18:55.000000000 +0000 @@ -0,0 +1,314 @@ +Backport of + +From d5c12ba890557a575c313bb3017910d7616fce3d Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Fri, 6 Oct 2023 17:11:40 +0100 +Subject: [PATCH] [PR #7661/85713a48 backport][3.8] Update Python parser for + RFCs 9110/9112 (#7662) + +--- python-aiohttp-3.8.4.orig/aiohttp/http_parser.py ++++ python-aiohttp-3.8.4/aiohttp/http_parser.py +@@ -60,16 +60,16 @@ __all__ = ( + + ASCIISET: Final[Set[str]] = 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: Final[Pattern[str]] = 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\\\\\"]") ++VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d).(\d)") ++HDRRE: Final[Pattern[bytes]] = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\"\\]") + + + class RawRequestMessage(NamedTuple): +@@ -148,8 +148,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: +@@ -170,6 +173,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: +@@ -204,10 +208,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)) + +@@ -322,15 +330,12 @@ class HttpParser(abc.ABC, Generic[_MsgT] + if length_hdr is None: + return None + +- try: +- length = int(length_hdr) +- except ValueError: ++ # Shouldn't allow +/- or other number formats. ++ # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 ++ if not length_hdr.strip(" \t").isdigit(): + raise InvalidHeader(CONTENT_LENGTH) + +- if length < 0: +- raise InvalidHeader(CONTENT_LENGTH) +- +- return length ++ return int(length_hdr) + + length = get_content_length() + # do not support old websocket spec +@@ -470,6 +475,24 @@ class HttpParser(abc.ABC, Generic[_MsgT] + 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: +@@ -523,7 +546,7 @@ class HttpRequestParser(HttpParser[RawRe + # 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 + +@@ -537,14 +560,10 @@ class HttpRequestParser(HttpParser[RawRe + 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))) + + if method == "CONNECT": + # authority-form, +@@ -611,12 +630,12 @@ class HttpResponseParser(HttpParser[RawR + def parse_message(self, lines: List[bytes]) -> RawResponseMessage: + 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 = "" + +@@ -632,13 +651,9 @@ class HttpResponseParser(HttpParser[RawR + 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 + ( +@@ -773,14 +788,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 +--- python-aiohttp-3.8.4.orig/tests/test_http_parser.py ++++ python-aiohttp-3.8.4/tests/test_http_parser.py +@@ -424,6 +424,74 @@ 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""" ++ with pytest.raises(http_exceptions.BadHttpMessage): ++ 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", ++ "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() ++ with pytest.raises(http_exceptions.InvalidHeader): ++ parser.feed_data(text) ++ ++ ++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 +@@ -605,6 +673,11 @@ def test_http_request_parser_bad_version + 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) +@@ -672,6 +745,11 @@ def test_http_response_parser_bad_versio + 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] + +@@ -686,17 +764,17 @@ def test_http_response_parser_bad(respon + + + 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 ++ with pytest.raises(http_exceptions.BadStatusLine): ++ response.feed_data(b"HTTP/1.1 99 test\r\n\r\n") + + + 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 -Nru python-aiohttp-3.8.4/debian/patches/CVE-2023-49081.patch python-aiohttp-3.8.4/debian/patches/CVE-2023-49081.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2023-49081.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2023-49081.patch 2024-11-28 19:33:27.000000000 +0000 @@ -0,0 +1,70 @@ +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) + +--- python-aiohttp-3.8.4.orig/aiohttp/client_reqrep.py ++++ python-aiohttp-3.8.4/aiohttp/client_reqrep.py +@@ -664,8 +664,8 @@ class ClientRequest: + 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) + +--- python-aiohttp-3.8.4.orig/tests/test_client_request.py ++++ python-aiohttp-3.8.4/tests/test_client_request.py +@@ -20,6 +20,7 @@ from aiohttp.client_reqrep import ( + _merge_ssl_params, + ) + from aiohttp.helpers import PY_311 ++from aiohttp.http import HttpVersion + from aiohttp.test_utils import make_mocked_coro + + +@@ -575,18 +576,18 @@ async def test_connection_header(loop, c + 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" +@@ -1113,6 +1114,19 @@ async def test_close(loop, buf, conn) -> + 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 -Nru python-aiohttp-3.8.4/debian/patches/CVE-2023-49082.patch python-aiohttp-3.8.4/debian/patches/CVE-2023-49082.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2023-49082.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2023-49082.patch 2024-11-28 19:31:12.000000000 +0000 @@ -0,0 +1,53 @@ +From 4075c653fb67a29740bf9ac050bb02d10a57343a Mon Sep 17 00:00:00 2001 +From: Ben Kallus <49924171+kenballus@users.noreply.github.com> +Date: Wed, 18 Oct 2023 12:18:35 -0400 +Subject: [PATCH] Backport 493f06797654c383242f0e8007f6e06b818a1fbc to 3.9 + (#7730) + +--- python-aiohttp-3.8.4.orig/aiohttp/http_parser.py ++++ python-aiohttp-3.8.4/aiohttp/http_parser.py +@@ -69,7 +69,9 @@ ASCIISET: Final[Set[str]] = set(string.p + # token = 1*tchar + METHRE: Final[Pattern[str]] = 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\"\\]" ++) + + + class RawRequestMessage(NamedTuple): +@@ -546,7 +548,7 @@ class HttpRequestParser(HttpParser[RawRe + # 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 + +--- python-aiohttp-3.8.4.orig/tests/test_http_parser.py ++++ python-aiohttp-3.8.4/tests/test_http_parser.py +@@ -441,6 +441,7 @@ def test_cve_2023_37276(parser: Any) -> + "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: +@@ -600,7 +601,13 @@ def test_http_request_bad_status_line(pa + 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 -Nru python-aiohttp-3.8.4/debian/patches/CVE-2024-23334-2.patch python-aiohttp-3.8.4/debian/patches/CVE-2024-23334-2.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2024-23334-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2024-23334-2.patch 2024-11-28 19:38:32.000000000 +0000 @@ -0,0 +1,12 @@ +Fix test + +--- python-aiohttp-3.8.4.orig/tests/test_web_urldispatcher.py ++++ python-aiohttp-3.8.4/tests/test_web_urldispatcher.py +@@ -13,6 +13,7 @@ import pytest + import yarl + + from aiohttp import abc, web ++from aiohttp.pytest_plugin import AiohttpClient + from aiohttp.web_urldispatcher import SystemRoute + + diff -Nru python-aiohttp-3.8.4/debian/patches/CVE-2024-23334.patch python-aiohttp-3.8.4/debian/patches/CVE-2024-23334.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2024-23334.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2024-23334.patch 2024-11-28 19:27:45.000000000 +0000 @@ -0,0 +1,190 @@ +From 9118a5831e8a65b8c839eb7e4ac983e040ff41df Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 28 Jan 2024 18:38:58 +0000 +Subject: [PATCH] [PR #8079/1c335944 backport][3.9] Validate static paths + (#8080) + +--- python-aiohttp-3.8.4.orig/aiohttp/web_urldispatcher.py ++++ python-aiohttp-3.8.4/aiohttp/web_urldispatcher.py +@@ -593,9 +593,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 +@@ -660,8 +665,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 +--- python-aiohttp-3.8.4.orig/docs/web_advanced.rst ++++ python-aiohttp-3.8.4/docs/web_advanced.rst +@@ -136,12 +136,22 @@ instead could be enabled with ``show_ind + + 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`` + +--- python-aiohttp-3.8.4.orig/docs/web_reference.rst ++++ python-aiohttp-3.8.4/docs/web_reference.rst +@@ -1836,9 +1836,15 @@ Router is any object that implements :cl + 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 +--- python-aiohttp-3.8.4.orig/tests/test_web_urldispatcher.py ++++ python-aiohttp-3.8.4/tests/test_web_urldispatcher.py +@@ -123,6 +123,97 @@ async def test_follow_symlink(tmp_dir_pa + assert (await r.text()) == data + + ++async def test_follow_symlink_directory_traversal( ++ tmp_path: pathlib.Path, aiohttp_client: AiohttpClient ++) -> 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: AiohttpClient ++) -> 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 -Nru python-aiohttp-3.8.4/debian/patches/CVE-2024-30251.patch python-aiohttp-3.8.4/debian/patches/CVE-2024-30251.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2024-30251.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2024-30251.patch 2024-11-24 14:12:09.000000000 +0000 @@ -0,0 +1,526 @@ +Combined backport of + +From cebe526b9c34dc3a3da9140409db63014bc4cf19 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sun, 7 Apr 2024 13:19:31 +0100 +Subject: [PATCH] Fix handling of multipart/form-data (#8280) (#8302) + +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) + +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) + +--- python-aiohttp-3.8.4.orig/aiohttp/formdata.py ++++ python-aiohttp-3.8.4/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 @@ class FormData: + 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 @@ class FormData: + "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)) +--- python-aiohttp-3.8.4.orig/aiohttp/multipart.py ++++ python-aiohttp-3.8.4/aiohttp/multipart.py +@@ -255,13 +255,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 + # TODO: typeing.Deque is not supported by Python 3.5 +@@ -329,6 +338,8 @@ class BodyPartReader: + 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: +@@ -444,7 +455,8 @@ class BodyPartReader: + """ + 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 + +@@ -478,7 +490,7 @@ class BodyPartReader: + """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]: +@@ -533,9 +545,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 +@@ -587,7 +607,24 @@ class MultipartReader: + 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: +@@ -623,19 +660,16 @@ class MultipartReader: + 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) + +@@ -726,6 +760,7 @@ class MultipartWriter(Payload): + super().__init__(None, content_type=ctype) + + self._parts: List[_Part] = [] ++ self._is_form_data = subtype == "form-data" + + def __enter__(self) -> "MultipartWriter": + return self +@@ -803,32 +838,38 @@ class MultipartWriter(Payload): + + 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 ( ++ 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) ++ # 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() ++ 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 +@@ -886,6 +927,11 @@ class MultipartWriter(Payload): + 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) + +--- python-aiohttp-3.8.4.orig/tests/test_client_functional.py ++++ python-aiohttp-3.8.4/tests/test_client_functional.py +@@ -1158,48 +1158,6 @@ async def test_POST_DATA_with_charset_po + 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() +@@ -1249,7 +1207,7 @@ async def test_POST_FILES(aiohttp_client + client = await aiohttp_client(app) + + 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() + +--- python-aiohttp-3.8.4.orig/tests/test_multipart.py ++++ python-aiohttp-3.8.4/tests/test_multipart.py +@@ -942,6 +942,58 @@ class TestMultipartReader: + 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 +@@ -1228,6 +1280,25 @@ class TestMultipartWriter: + part = writer._parts[0][0] + assert part.headers[CONTENT_TYPE] == "test/passed" + ++ def test_set_content_disposition_after_append(self): ++ writer = aiohttp.MultipartWriter("form-data") ++ 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: + writer.append("foo") +@@ -1278,7 +1349,6 @@ class TestMultipartWriter: + CONTENT_TYPE: "text/python", + }, + ) +- content_length = part.size + await writer.write(stream) + + assert part.headers[CONTENT_TYPE] == "text/python" +@@ -1289,9 +1359,7 @@ class TestMultipartWriter: + 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): +@@ -1305,7 +1373,6 @@ class TestMultipartWriter: + CONTENT_TYPE: "text/python", + }, + ) +- content_length = part.size + await writer.write(stream) + + assert part.headers[CONTENT_TYPE] == "text/python" +@@ -1316,9 +1383,7 @@ class TestMultipartWriter: + 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): +@@ -1330,8 +1395,6 @@ class TestMultipartWriter: + headers={CONTENT_TYPE: "text/plain"}, + ) + +- content_length = part.size +- + assert CONTENT_DISPOSITION in part.headers + + part.set_content_disposition("attachments", filename="bug.py") +@@ -1344,9 +1407,7 @@ class TestMultipartWriter: + 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"' + ) + + +--- python-aiohttp-3.8.4.orig/tests/test_web_functional.py ++++ python-aiohttp-3.8.4/tests/test_web_functional.py +@@ -34,7 +34,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 + + +@@ -429,25 +430,6 @@ async def test_release_post_data(aiohttp + 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() +@@ -505,7 +487,8 @@ async def test_100_continue(aiohttp_clie + 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) +@@ -683,7 +666,7 @@ async def test_upload_file(aiohttp_clien + 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 -Nru python-aiohttp-3.8.4/debian/patches/CVE-2024-52304.patch python-aiohttp-3.8.4/debian/patches/CVE-2024-52304.patch --- python-aiohttp-3.8.4/debian/patches/CVE-2024-52304.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/CVE-2024-52304.patch 2024-11-28 19:20:16.000000000 +0000 @@ -0,0 +1,86 @@ +From 259edc369075de63e6f3a4eaade058c62af0df71 Mon Sep 17 00:00:00 2001 +From: "J. Nick Koston" +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) + +--- python-aiohttp-3.8.4.orig/aiohttp/http_parser.py ++++ python-aiohttp-3.8.4/aiohttp/http_parser.py +@@ -785,6 +785,13 @@ class HttpPayloadParser: + i = chunk.find(CHUNK_EXT, 0, pos) + if i >= 0: + size_b = chunk[:i] # strip chunk-extensions ++ # Verify no LF in the chunk-extension ++ 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] + +--- python-aiohttp-3.8.4.orig/tests/test_http_parser.py ++++ python-aiohttp-3.8.4/tests/test_http_parser.py +@@ -11,6 +11,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, +@@ -882,6 +883,53 @@ def test_parse_no_length_payload(parser) + msg, payload = parser.feed_data(text)[0][0] + assert payload.is_eof() + ++@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 ++) -> None: ++ """Test the C-parser with a chunked payload that has a LF in the chunk extensions.""" ++ # The C parser will raise a BadHttpMessage from feed_data ++ parser = HttpRequestParserC( ++ protocol, ++ loop, ++ 2**16, ++ max_line_size=8190, ++ max_field_size=8190, ++ ) ++ payload = ( ++ b"GET / HTTP/1.1\r\nHost: localhost:5001\r\n" ++ b"Transfer-Encoding: chunked\r\n\r\n2;\nxx\r\n4c\r\n0\r\n\r\n" ++ b"GET /admin HTTP/1.1\r\nHost: localhost:5001\r\n" ++ b"Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n" ++ ) ++ 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: ++ """Test the py-parser with a chunked payload that has a LF in the chunk extensions.""" ++ # The py parser will not raise the BadHttpMessage directly, but instead ++ # it will set the exception on the StreamReader. ++ parser = HttpRequestParserPy( ++ protocol, ++ loop, ++ 2**16, ++ max_line_size=8190, ++ max_field_size=8190, ++ ) ++ payload = ( ++ b"GET / HTTP/1.1\r\nHost: localhost:5001\r\n" ++ b"Transfer-Encoding: chunked\r\n\r\n2;\nxx\r\n4c\r\n0\r\n\r\n" ++ b"GET /admin HTTP/1.1\r\nHost: localhost:5001\r\n" ++ b"Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n" ++ ) ++ messages, _, _ = parser.feed_data(payload) ++ reader = messages[0][1] ++ assert isinstance(reader.exception(), http_exceptions.BadHttpMessage) ++ assert "\\nxx" in str(reader.exception()) ++ + + def test_partial_url(parser) -> None: + messages, upgrade, tail = parser.feed_data(b"GET /te") diff -Nru python-aiohttp-3.8.4/debian/patches/series python-aiohttp-3.8.4/debian/patches/series --- python-aiohttp-3.8.4/debian/patches/series 2023-02-16 10:54:52.000000000 +0000 +++ python-aiohttp-3.8.4/debian/patches/series 2024-11-28 19:38:32.000000000 +0000 @@ -2,3 +2,10 @@ 0002-Add-shebang-to-examples.patch 0003-remove-forkme-button-from-docs-to-prevent-privacy-br.patch 0004-remove-sphinxcontrib.towncrier-for-now.patch +CVE-2024-30251.patch +CVE-2023-47627.patch +CVE-2024-52304.patch +CVE-2024-23334.patch +CVE-2023-49082.patch +CVE-2023-49081.patch +CVE-2024-23334-2.patch