From patchwork Fri Nov 6 07:26:56 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103226 Received: from rcdn-iport-5.cisco.com (rcdn-iport-5.cisco.com [173.37.86.76]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1306.1793950037587061845 for ; Thu, 05 Nov 2026 23:27:18 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=bkSqX771; spf=pass (domain: cisco.com, ip: 173.37.86.76, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=18355; q=dns/txt; s=iport01; t=1793950037; x=1795159637; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=z5drUIZ0YhwiiJnpBFWcSF/J6Lc2TlysbvpbCg1ucyw=; b=bkSqX771C1elZFtXBtLkiv7kGRQowrKqOBrH0rVi+bi/yi4iwS+30OHX 9vDK4ugM079g4DxNbN+eIcHOdNFviejozO6hRMhZo3Ml7r1wavmqKgzOI j93uzA8yEchqRi0h6KpHISU6rWKk4B2p1a5fkpUUAFXfTjoObcIZnSpUY NRxn4XF89GnfJxosizjS6VtHWN/HUY/qqFWsjW9UGEPwCNZYHYVyT70+N SeWt8Uz9K6HXVjJQPq+V+ps+7L3RqclzdXvgcu8jB8oQZAabwdHZEKoZ5 3FVLrIedeRPmM3RnOPgV+vaHgffb8s62cLdshdKvouKPndbMT5gdercr9 A==; X-CSE-ConnectionGUID: 5YIJb2DCRIe8r/cZ97ucEA== X-CSE-MsgGUID: QJVy+yQBRoyYjLjhbezkKA== X-IPAS-Result: A0BLAgDmf+1q/5L/Ja1QCh4BAQsSDIIFC4IYP3VgQ0kDlkgDgROQOYxVgX4PAQEBD0QNBAEBhD9GAo4dAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQEKAQEFAQEBAgEHBYEOE4YVBjQNkBIBAgEDJwsBGAEtEBwDAQIvKyMIEAmCKlgBgnQDEbwSgXkzgQGDKQGBVNszAQsUAYE4hUCII10YAYQCeycbG4FygRWBO4EdG3aBBYFcAQECAYEqBgWGbgSCIoEMgVqNGAKBUYZ5SIEeA1ksAVUTDQoLBwWBaAM1EioVbjIdgSM+F4EHGwYFgR2BHoESgQddgSsoXQEhgQiBdgKCUYIAAgFJQQ4HRVMJJUMSRyQiCBIJARMaLAqBGjU8CSY/FQxkCxoFYQgCDQx0ORAwHiQvBRwmPhsKLxgNSBEsNxUZBD5uB5I9gmUtAQ0fYQEKDhEBAX8GDA8UGV8cAjCSaYQii3yCIaERCiiDeIwklToaM4QEgVeSQpJUC5h9glmLMZVpGANNhGmBaDyBWXAVgyIJShkPhlGHWgMLC4NghRPHHiYyAgkDLwEBBwIHDgMLgWiQAAEnBHJgAQE IronPort-Data: A9a23:9J2VZquhFxYT+XnwAdiXo3UphefnVAZfMUV32f8akzHdYApBsoF/q tZmKTiFPPyDM2L0fd8lYdmxphwF6JHQz9Q2SFZq/H1hFSkTgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrb/656yYsjclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZzdJ5xYuajhKsvvb9Us21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIwp9dWI2sU2 dciCT1ddFeItbPp3bmWY7w57igjBJGD0II3oHpsy3TdSP0hW52GG/SM7t5D1zB2jcdLdRrcT 5NGMnw0M1KaPkAJYwtJYH49tL/Aan3Xfz5VrFuUtKMf6GnIxws327/oWDbQUoHWGJ8FzhrB/ woq+UzAIC8Ld82m4wCC40Ouou3Ply7pW6E7QejQGvlCxQf7KnYoIBoOWF22pPO0hkKzV5dTJ lIZ/gIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sZOZottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvoUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:40Kwu60XEXjWmrnp8tqsKAqjBJAkLtp133Aq2lEZdPWaSKOlfq eV7ZMmPHDP6Qr5NEtMpTnEAtjjfZq+z+8Q3WBuB9eftWDd0QPCRr2Kr7GSpgEIcBeRygcy78 tdmtBFeb7NJGk/q9rm6w+lFNtl6tyG/Ke0wdr69R5WPGdXg2UK1XYANu5deXcGPTV7OQ== X-Talos-CUID: 9a23:ARISUGOO8Jfh9O5DBRhG1kRONusfU3CN1jDbER7nFG1EYejA X-Talos-MUID: 9a23:AZw1jQRIWq0+eg01RXTVpg9zd9ZRyZ6pGXEPtZcGscSWNg9ZbmI= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="545288411" Received: from rcdn-l-core-09.cisco.com ([173.37.255.146]) by rcdn-iport-5.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:16 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-09.cisco.com (Postfix) with ESMTPS id 782961800059F; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 23AB7CBF201; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 1/7] python3-aiohttp: fix CVE-2026-111066 Date: Thu, 5 Nov 2026 23:26:56 -0800 Message-Id: <20261106072702.502813-2-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-09.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:19 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131967 From: Darsh Kelaiya Backport the multipart descriptor-exhaustion fix [1] to aiohttp 3.13.5. Spool small file parts in memory and count raw multipart bytes, including headers, against client_max_size. Include the upstream descriptor-test correction [2] so unrelated file descriptors do not make the regression test unreliable. Adapt the HTTP 413 constructor and retain the existing read size. The vulnerability and fixed release are documented in [3]. [1] https://github.com/aio-libs/aiohttp/commit/2d94633b7f1b4c1ba07d647b629a8440652b2d09 [2] https://github.com/aio-libs/aiohttp/commit/011c08204197beba8281911907b59ffcecea1413 [3] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-9xw5-w5jj-pvcg Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111066_p1.patch | 400 ++++++++++++++++++ .../python3-aiohttp/CVE-2026-111066_p2.patch | 57 +++ .../python/python3-aiohttp_3.13.5.bb | 2 + 3 files changed, 459 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p1.patch new file mode 100644 index 0000000000..8b6e29bbfb --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p1.patch @@ -0,0 +1,400 @@ +From 2d94633b7f1b4c1ba07d647b629a8440652b2d09 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Tue, 18 Aug 2026 21:54:45 +0100 +Subject: [PATCH] Reduce number of file descriptors needed in multipart + messages (#13426) (#13475) + +(cherry picked from commit c92ed4c48dafd40d5ed4c86587af9e081ac7d971) + +CVE: CVE-2026-111066 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/2d94633b7f1b4c1ba07d647b629a8440652b2d09] + +Backport Changes: +- Pass the raw payload size to the older HTTP 413 constructor and + update the existing test to match upstream's size-limit assertion. +- Keep the existing type annotations and surrounding test signatures. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13426.bugfix.rst | 1 + + aiohttp/web_request.py | 96 +++++++++++++++----- + tests/test_web_functional.py | 167 ++++++++++++++++++++++++++++++++++- + 3 files changed, 237 insertions(+), 27 deletions(-) + create mode 100644 CHANGES/13426.bugfix.rst + +diff --git a/CHANGES/13426.bugfix.rst b/CHANGES/13426.bugfix.rst +new file mode 100644 +index 0000000..8b9ee29 +--- /dev/null ++++ b/CHANGES/13426.bugfix.rst +@@ -0,0 +1 @@ ++Switched multipart handling to use spooled temporary files to reduce number of file descriptors needed -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py +index f2d4c51..e978b2a 100644 +--- a/aiohttp/web_request.py ++++ b/aiohttp/web_request.py +@@ -4,11 +4,12 @@ import io + import re + import socket + import string +-import tempfile ++import sys + import types + import warnings + from types import MappingProxyType + from typing import ( ++ IO, + TYPE_CHECKING, + Any, + Dict, +@@ -64,6 +65,30 @@ from .typedefs import ( + from .web_exceptions import HTTPRequestEntityTooLarge + from .web_response import StreamResponse + ++if sys.version_info >= (3, 11): ++ from tempfile import SpooledTemporaryFile ++else: ++ import tempfile ++ ++ class SpooledTemporaryFile(tempfile.SpooledTemporaryFile[bytes], io.IOBase): ++ """Make the spooled file satisfy the documented `FileField.file` type. ++ ++ `tempfile.SpooledTemporaryFile` only became an `io.IOBase` subclass in ++ 3.11 (python/cpython#70363), and never grew the three capability ++ predicates before then. Both underlying files (`io.BytesIO` before ++ rollover, a `w+b` temp file after) are readable, writable and seekable. ++ """ ++ ++ def readable(self) -> bool: ++ return True ++ ++ def writable(self) -> bool: ++ return True ++ ++ def seekable(self) -> bool: ++ return True ++ ++ + __all__ = ("BaseRequest", "FileField", "Request") + + +@@ -73,11 +98,18 @@ if TYPE_CHECKING: + from .web_urldispatcher import UrlMappingMatchInfo + + ++# Multipart file parts are buffered in memory and only spilled to a temporary ++# file once they outgrow this size. A temp file per part would let a body made ++# of many tiny parts exhaust the process's file descriptors; spooling bounds ++# that at ``client_max_size // _FILE_SPOOL_MAX_SIZE`` descriptors per request. ++_FILE_SPOOL_MAX_SIZE: Final[int] = 1024**2 ++ ++ + @attr.s(auto_attribs=True, frozen=True, slots=True) + class FileField: + name: str + filename: str +- file: io.BufferedReader ++ file: IO[bytes] + content_type: str + headers: CIMultiDictProxy[str] + +@@ -726,8 +758,15 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + multipart = await self.multipart() + max_size = self._client_max_size + +- size = 0 ++ payload = self._payload + while (field := await multipart.next()) is not None: ++ # This check is needed for empty payloads, which still add ++ # overhead without entering the loop and the check below. ++ if 0 < max_size < payload.total_bytes: ++ raise HTTPRequestEntityTooLarge( ++ max_size=max_size, actual_size=payload.total_bytes ++ ) ++ + field_ct = field.headers.get(hdrs.CONTENT_TYPE) + + if isinstance(field, BodyPartReader): +@@ -739,22 +778,37 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + # present. + # https://tools.ietf.org/html/rfc7578#section-4.4 + if field.filename: +- # store file in temp file +- tmp = await self._loop.run_in_executor( +- None, tempfile.TemporaryFile +- ) ++ tmp = SpooledTemporaryFile(_FILE_SPOOL_MAX_SIZE) ++ # rolled means the temp file now uses the disk, at ++ # which point we want to run in the executor. ++ rolled = False + while chunk := await field.read_chunk(size=2**18): ++ # Bounds one part, mid-read. ++ if 0 < max_size < payload.total_bytes: ++ if rolled: ++ await self._loop.run_in_executor(None, tmp.close) ++ else: ++ tmp.close() ++ raise HTTPRequestEntityTooLarge( ++ max_size=max_size, actual_size=payload.total_bytes ++ ) + async for decoded_chunk in field.decode_iter(chunk): +- await self._loop.run_in_executor( +- None, tmp.write, decoded_chunk ++ # Update before writing, so we know if next ++ # write is going to hit the disk. ++ rolled = rolled or ( ++ tmp.tell() + len(decoded_chunk) ++ > _FILE_SPOOL_MAX_SIZE + ) +- size += len(decoded_chunk) +- if 0 < max_size < size: +- await self._loop.run_in_executor(None, tmp.close) +- raise HTTPRequestEntityTooLarge( +- max_size=max_size, actual_size=size ++ if rolled: ++ await self._loop.run_in_executor( ++ None, tmp.write, decoded_chunk + ) +- await self._loop.run_in_executor(None, tmp.seek, 0) ++ else: ++ tmp.write(decoded_chunk) ++ if rolled: ++ await self._loop.run_in_executor(None, tmp.seek, 0) ++ else: ++ tmp.seek(0) + + if field_ct is None: + field_ct = "application/octet-stream" +@@ -762,7 +816,7 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + ff = FileField( + field.name, + field.filename, +- cast(io.BufferedReader, tmp), ++ tmp, + field_ct, + field.headers, + ) +@@ -771,10 +825,9 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + # deal with ordinary data + raw_data = bytearray() + while chunk := await field.read_chunk(): +- size += len(chunk) +- if 0 < max_size < size: ++ if 0 < max_size < payload.total_bytes: + raise HTTPRequestEntityTooLarge( +- max_size=max_size, actual_size=size ++ max_size=max_size, actual_size=payload.total_bytes + ) + raw_data.extend(chunk) + +@@ -843,10 +896,7 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + if self._post is None or self.content_type != "multipart/form-data": + return + +- # NOTE: Release file descriptors for the +- # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom` +- # NOTE: instances of files sent within multipart request body +- # NOTE: via HTTP POST request. ++ # Release the temp files created within multipart request body. + for file_name, file_field_object in self._post.items(): + if isinstance(file_field_object, FileField): + file_field_object.file.close() +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index 0d2824a..a314d9b 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -1,6 +1,8 @@ + import asyncio ++import base64 + import io + import json ++import os + import pathlib + import socket + import sys +@@ -29,6 +31,7 @@ from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer + from aiohttp.streams import StreamReader + from aiohttp.typedefs import Handler + from aiohttp.web_protocol import MAX_MSG_QUEUE_SIZE, RequestHandler ++from aiohttp.web_request import _FILE_SPOOL_MAX_SIZE + + try: + import brotlicffi as brotli +@@ -2023,10 +2026,7 @@ async def test_post_max_client_size(aiohttp_client) -> None: + + assert 413 == resp.status + resp_text = await resp.text() +- assert ( +- "Maximum request body size 10 exceeded, " +- "actual body size 1024" in resp_text +- ) ++ assert "Maximum request body size 10 exceeded" in resp_text + data["file"].close() + + await resp.release() +@@ -2050,6 +2050,165 @@ async def test_post_max_client_size_for_file(aiohttp_client) -> None: + await resp.release() + + ++def _multipart_file_parts(count: int, body: bytes = b"") -> bytes: ++ """Raw multipart/form-data body of ``count`` file parts, boundary ``b``.""" ++ return ( ++ b"".join( ++ b'--b\r\nContent-Disposition: form-data; name="f%d"; ' ++ b'filename="x"\r\n\r\n' % index + body + b"\r\n" ++ for index in range(count) ++ ) ++ + b"--b--\r\n" ++ ) ++ ++ ++async def test_post_max_client_size_counts_multipart_headers( ++ aiohttp_client: AiohttpClient, ++) -> None: ++ async def handler(request: web.Request) -> NoReturn: ++ await request.post() ++ assert False ++ ++ app = web.Application(client_max_size=1) ++ app.router.add_post("/", handler) ++ client = await aiohttp_client(app) ++ ++ body = _multipart_file_parts(55) ++ assert len(body) > 1 ++ async with client.post( ++ "/", data=body, headers={CONTENT_TYPE: "multipart/form-data; boundary=b"} ++ ) as resp: ++ assert resp.status == 413 ++ ++ ++@pytest.mark.parametrize( ++ "max_size", ++ (_FILE_SPOOL_MAX_SIZE, 2 * _FILE_SPOOL_MAX_SIZE), ++ ids=("spooled", "rolled-over"), ++) ++async def test_post_max_client_size_within_single_part( ++ aiohttp_client: AiohttpClient, max_size: int ++) -> None: ++ async def handler(request: web.Request) -> NoReturn: ++ await request.post() ++ assert False ++ ++ app = web.Application(client_max_size=max_size) ++ app.router.add_post("/", handler) ++ client = await aiohttp_client(app) ++ ++ # A sole oversized part is never followed by a boundary, so only the ++ # per-chunk check can reject it, and it must do so before buffering. The ++ # larger limit lets the part reach the disk first, so the spool is then ++ # closed off the event loop too. ++ data = FormData() ++ with io.BytesIO(b"x" * (3 * _FILE_SPOOL_MAX_SIZE)) as file_handle: ++ data.add_field("file", file_handle, filename="x.bin") ++ async with client.post("/", data=data) as resp: ++ assert resp.status == 413 ++ ++ ++@pytest.mark.parametrize("filename", (b"", b'; filename="x"'), ids=("field", "file")) ++async def test_post_max_client_size_counts_undecoded_bytes( ++ aiohttp_client: AiohttpClient, filename: bytes ++) -> None: ++ async def handler(request: web.Request) -> NoReturn: ++ await request.post() ++ assert False ++ ++ limit = 64 * 1024 ++ app = web.Application(client_max_size=limit) ++ app.router.add_post("/", handler) ++ client = await aiohttp_client(app) ++ ++ # base64 decodes to exactly the limit but arrives 4/3 larger. The limit is ++ # on the request body, so both kinds of part must reject it alike. ++ encoded = base64.b64encode(b"A" * limit) ++ assert len(encoded) > limit ++ body = ( ++ b'--b\r\nContent-Disposition: form-data; name="f"' + filename + b"\r\n" ++ b"Content-Transfer-Encoding: base64\r\n\r\n" + encoded + b"\r\n--b--\r\n" ++ ) ++ async with client.post( ++ "/", data=body, headers={CONTENT_TYPE: "multipart/form-data; boundary=b"} ++ ) as resp: ++ assert resp.status == 413 ++ ++ ++@pytest.mark.skipif(not os.path.isdir("/dev/fd"), reason="needs /dev/fd to count fds") ++@pytest.mark.parametrize( ++ ("parts", "part_body", "expected_fds"), ++ ( ++ (55, b"payload", 0), ++ (1, b"x" * (_FILE_SPOOL_MAX_SIZE + 1), 1), ++ ), ++ ids=("many-small-parts", "one-oversized-part"), ++) ++async def test_post_file_fields_descriptor_cost( ++ aiohttp_client: AiohttpClient, parts: int, part_body: bytes, expected_fds: int ++) -> None: ++ """Only a part past the spool size may cost a descriptor.""" ++ ++ async def handler(request: web.Request) -> web.Response: ++ before = set(await asyncio.to_thread(os.listdir, "/dev/fd")) ++ data = await request.post() ++ after = set(await asyncio.to_thread(os.listdir, "/dev/fd")) ++ assert len(data) == parts ++ return web.Response(text=str(len(after - before))) ++ ++ app = web.Application(client_max_size=8 * 1024**2) ++ app.router.add_post("/", handler) ++ client = await aiohttp_client(app) ++ ++ with io.BytesIO(_multipart_file_parts(parts, part_body)) as body: ++ async with client.post( ++ "/", ++ data=body, ++ headers={CONTENT_TYPE: "multipart/form-data; boundary=b"}, ++ ) as resp: ++ assert resp.status == 200 ++ assert await resp.text() == str(expected_fds) ++ ++ ++@pytest.mark.parametrize( ++ "size", ++ ( ++ 0, ++ 1, ++ _FILE_SPOOL_MAX_SIZE - 1, ++ _FILE_SPOOL_MAX_SIZE, ++ _FILE_SPOOL_MAX_SIZE + 1, ++ ), ++) ++async def test_post_file_field_spool_rollover( ++ aiohttp_client: AiohttpClient, size: int ++) -> None: ++ """A part is byte-identical either side of the spool/temp-file boundary.""" ++ payload = (b"0123456789abcdef" * (size // 16 + 1))[:size] ++ ++ async def handler(request: web.Request) -> web.Response: ++ field = (await request.post())["file"] ++ assert isinstance(field, aiohttp.web_request.FileField) ++ assert isinstance(field.file, io.IOBase) ++ assert field.file.readable() ++ assert field.file.writable() ++ assert field.file.seekable() ++ content = await asyncio.to_thread(field.file.read) ++ assert len(content) == size ++ assert content == payload ++ return web.Response() ++ ++ app = web.Application(client_max_size=8 * 1024**2) ++ app.router.add_post("/", handler) ++ client = await aiohttp_client(app) ++ ++ data = FormData() ++ with io.BytesIO(payload) as file_handle: ++ data.add_field("file", file_handle, filename="x.bin") ++ async with client.post("/", data=data) as resp: ++ assert resp.status == 200 ++ ++ + async def test_response_with_bodypart(aiohttp_client) -> None: + async def handler(request): + reader = await request.multipart() diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p2.patch new file mode 100644 index 0000000000..9b9345f577 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111066_p2.patch @@ -0,0 +1,57 @@ +From 011c08204197beba8281911907b59ffcecea1413 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Wed, 26 Aug 2026 02:39:52 +0100 +Subject: [PATCH] [PR #13549/4602990f backport][3.14] Fix flaky descriptor test + (#13551) + +**This is a backport of PR #13549 as merged into master +(4602990f60241f759f861a3a4c1243ba2294ab53).** + +Co-authored-by: Sam Bull + +CVE: CVE-2026-111066 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/011c08204197beba8281911907b59ffcecea1413] + +Signed-off-by: Darsh Kelaiya +--- + tests/test_web_functional.py | 20 +++++++++++++++++--- + 1 file changed, 17 insertions(+), 3 deletions(-) + +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index a314d9b..9fafd52 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -2135,7 +2135,9 @@ async def test_post_max_client_size_counts_undecoded_bytes( + assert resp.status == 413 + + +-@pytest.mark.skipif(not os.path.isdir("/dev/fd"), reason="needs /dev/fd to count fds") ++@pytest.mark.skipif( ++ not os.path.isdir("/proc/self/fd"), reason="needs /proc/self/fd to identify fds" ++) + @pytest.mark.parametrize( + ("parts", "part_body", "expected_fds"), + ( +@@ -2150,8 +2152,20 @@ async def test_post_file_fields_descriptor_cost( + """Only a part past the spool size may cost a descriptor.""" + ++ def deleted_file_fds() -> set[tuple[str, str]]: ++ """(fd, symlink target) pairs for open fds backed by deleted files.""" ++ fds = set() ++ for name in os.listdir("/proc/self/fd"): ++ try: ++ target = os.readlink(f"/proc/self/fd/{name}") ++ except OSError: ++ continue # closed between listdir and readlink ++ if target.endswith("(deleted)"): ++ fds.add((name, target)) ++ return fds ++ + async def handler(request: web.Request) -> web.Response: +- before = set(await asyncio.to_thread(os.listdir, "/dev/fd")) ++ before = await asyncio.to_thread(deleted_file_fds) + data = await request.post() +- after = set(await asyncio.to_thread(os.listdir, "/dev/fd")) ++ after = await asyncio.to_thread(deleted_file_fds) + assert len(data) == parts + return web.Response(text=str(len(after - before))) diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index bd5b5fe4d9..917405c715 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -22,6 +22,8 @@ SRC_URI += " \ file://CVE-2026-59881.patch \ file://CVE-2026-69243.patch \ file://CVE-2026-69244.patch \ + file://CVE-2026-111066_p1.patch \ + file://CVE-2026-111066_p2.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:26:57 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103230 Received: from rcdn-iport-4.cisco.com (rcdn-iport-4.cisco.com [173.37.86.75]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1304.1793950037466876407 for ; Thu, 05 Nov 2026 23:27:17 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=JPzCIPZm; spf=pass (domain: cisco.com, ip: 173.37.86.75, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=4924; q=dns/txt; s=iport01; t=1793950037; x=1795159637; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=6CHElhsShU/wqSUhdsglxtNA9+NUT5Zs/ifeCqxmzxI=; b=JPzCIPZm2ZwMxP6xWfY76D/snP+OnQW28F0V2ljhvZayQcu9UjH4tKrb IKYZD9h8CNE0HrjZozsylODa0kspc5JbFz3kHRhbUKnylrkuwkshlbQAs Q/ucrpw6vdA1txpKlLyrJIIDXsm9UGaos7OCSJvQyxspXxNpeuJQtZ2LV ni1m7/ul8mfWUosZRZq5we9J5oTcCR8LamzbRdGi8UGggWiIjt/4TSTox ERDBdBcIIkURGt4RBHpGtMhEqW6YJnZBg+P8AOZQ0FPo5sk5Q1s78Gi8e qlBB092MJ8WhJbLi9u5BZvDpKbAgX6lmuaOSdakat6seCjmgj246ERGN/ g==; X-CSE-ConnectionGUID: 1/BQBtxBQlKk7+deIkbn6g== X-CSE-MsgGUID: 9j2lQFfoTqq0miQ/NcljkQ== X-IPAS-Result: A0BJAgDmf+1q/4v/Ja1aglmCV3VgQ0kDlkgDkUyMVYF+DwEBAQ9EDQQBAYQ/RgKOHQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMnCwEYAS0QHAMBAi8rIwgZgwIBgnQDEbwSgXkzgQGDKQGBVNszAQsUAYE4hUCII10YAYR9JxsbgXKBFYE7gi6BBYFcAgKBT4ZVBIIigQyBWo0YAoFRhnlIgR4DWSwBVRMNCgsHBYFoAzUSKhVuMh2BIz4XgQcbBgWBHYEegRKBB12BKyhdASGBCIF2AoJRggACAUlBDgdFUwklQxJHJCIIEgkBExosCoEaNTwJJj8VDGQLGgVhCAINDHQ5EDAeJC8FHCY+GwovGA1IESw3FRkEPm4Hkj2DEoEOASoBgTR4FgajOYIhoREKKIN4jCSVOhozqnELmH2OCpYBA02EaYFoPIFZcBWDIglKGQ+OOYNrhRPHHiYyAgEIAy8BAQcCBw4DC4FokAKBfAEB IronPort-Data: A9a23:UnGauKq1fZ4ssbtKr0U9wK8zCqpeBmJIZBIvgKrLsJaIsI4StFCzt garIBnQMq2CZDHzLd4lYI7j8UxTvZfQzdUxSws4+X0wEX4R+OPIVI+TRqvS04x+DSFioGZPt Zh2hgzodZhsJpPkjk7zdOWn9T8jhfngqoPUUIbsIjp2SRJvVBAvgBdin/9RqoNziLBVOSvV0 T/Ji5OZYgDNNwJcaDpOtfva8E435ZwehRtB1rAATaET1LPhvyF94KI3fcmZM3b+S49IKe+2L 86r5K255G7Q4yA2AdqjlLvhGmVSKlIFFVHT4pb+c/HKbilq/kTe4I5iXBYvQRs/ZwGyojxE4 I4lWapc5useFvakdOw1C3G0GszlVEFM0OevzXOX6aR/w6BaGpfh660GMa04AWEX0r8wK1hk6 ewyETQubU/Ai++Xw7i9FOY506zPLOGzVG8ekmtrwTecCbMtRorOBv2Qo9RZxzw3wMtJGJ4yZ eJANmEpN0uGOUASfA5MWfrSn8/w7pX7WzFbpVacpLs+y2PS1wd2lrPqNbI5f/TXGZgNxBnA9 juuE2LRUxNAMOWxxz2562v0g+722gXwXIARLejtnhJtqBjJroAJMzURTVa9rPyzh0KyVt4aJ 0EK9y4Gqakp6FftScHwWRC9qnOIshMQHd1KHIUHBBql0KHY5UOdQ2MDVDMEMIZgv84tTjts3 ViM9z/0OQFSXHSuYSr13t+pQfmaYED58Udqifc4cDY4 IronPort-HdrOrdr: A9a23:eGBBTKwVHyCBWnvBdkMXKrPwDr1zdoMgy1knxilNoNJuHfBw8P re+8jzuiWUtN98YhwdcJW7Scu9qBDnhPpICPcqXYtKNTOO0ADDEGgh1/qG/9SKIUPDH4BmuZ uIC5IOa+HYPBxdkdvw5hW+HpILxdmK973tuMLlpk0dKz2Dr8pbnn9E4sHxKDwOeDV7 X-Talos-CUID: 9a23:NxmDYmzIsHDdMTTKHDGfBgVNXeocWGHN5k7UAEznSmpkab6fTwGfrfY= X-Talos-MUID: 9a23:f0+rKg1Y03y2ja/xMDwidMMpXDUjsqb0DHkjn5I6otDcMC1JfBGWiDa0e9py X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="545027081" Received: from rcdn-l-core-02.cisco.com ([173.37.255.139]) by rcdn-iport-4.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:16 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-02.cisco.com (Postfix) with ESMTPS id 7BA5918000207; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 27CCDCBF202; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 2/7] python3-aiohttp: fix CVE-2026-111750 Date: Thu, 5 Nov 2026 23:26:57 -0800 Message-Id: <20261106072702.502813-3-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-02.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:20 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131964 From: Darsh Kelaiya Backport the upstream HEAD request parsing fix [1] to aiohttp 3.13.5, as identified by [2]. Only responses to HEAD may skip their body; incoming HEAD requests must consume the declared request body. Retain the upstream regression that embeds a request in a HEAD body and verifies that the following request remains correctly framed. The pure-Python parser remains available with extensions installed. [1] https://github.com/aio-libs/aiohttp/commit/9b27347e61454d9f8ca2055f4c67a37dee336e83 [2] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-pqxq-w44r-73xw Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111750.patch | 83 +++++++++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 1 + 2 files changed, 84 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111750.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111750.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111750.patch new file mode 100644 index 0000000000..61498e09f3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111750.patch @@ -0,0 +1,83 @@ +From 9b27347e61454d9f8ca2055f4c67a37dee336e83 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Wed, 9 Sep 2026 01:25:13 +0100 +Subject: [PATCH] Fix head request parsing (#13671) (#13673) + +(cherry picked from commit 5789546e37fe35af27d0a2f840870b22bc15bfc4) + +CVE: CVE-2026-111750 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/9b27347e61454d9f8ca2055f4c67a37dee336e83] + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13671.bugfix.rst | 1 + + aiohttp/http_parser.py | 6 +++++- + tests/test_http_parser.py | 27 ++++++++++++++++++++++++++- + 3 files changed, 32 insertions(+), 2 deletions(-) + create mode 100644 CHANGES/13671.bugfix.rst + +diff --git a/CHANGES/13671.bugfix.rst b/CHANGES/13671.bugfix.rst +new file mode 100644 +index 0000000..e6fbc70 +--- /dev/null ++++ b/CHANGES/13671.bugfix.rst +@@ -0,0 +1 @@ ++Fixed pure-Python request parser not reading a body in a ``HEAD`` request -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index f4458dc..ce15764 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -426,8 +426,12 @@ class HttpParser(abc.ABC, Generic[_MsgT]): + + assert self.protocol is not None + # calculate payload ++ # https://www.rfc-editor.org/info/rfc9112/#name-message-body-length ++ # https://www.rfc-editor.org/info/rfc9110/#section-9.3.1-6 ++ # EMPTY_BODY_METHODS should only apply to responses. ++ # self.method is None on request parser. + empty_body = code in EMPTY_BODY_STATUS_CODES or bool( +- method and method in EMPTY_BODY_METHODS ++ self.method and self.method in EMPTY_BODY_METHODS + ) + if not empty_body and ( + (length is not None and length > 0) or msg.chunked +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 4cdd94c..4255f4d 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -1781,7 +1781,32 @@ def test_http_request_chunked_payload_and_next_message(parser) -> None: + assert not payload2.is_eof() + + +-def test_http_request_chunked_payload_chunks(parser) -> None: ++def test_http_request_parser_head_with_content_length_payload( ++ parser: HttpRequestParser, ++) -> None: ++ smuggled = b"GET /smuggled HTTP/1.1\r\nHost: a\r\n\r\n" ++ text = ( ++ b"HEAD /test HTTP/1.1\r\nHost: a\r\nContent-Length: %d\r\n\r\n" % len(smuggled) ++ + smuggled ++ + b"POST /next HTTP/1.1\r\nHost: a\r\nContent-Length: 0\r\n\r\n" ++ ) ++ messages, upgraded, tail = parser.feed_data(text) ++ ++ assert len(messages) == 2 ++ msg, payload = messages[0] ++ assert msg.method == "HEAD" ++ assert b"".join(payload._buffer) == smuggled ++ assert payload.is_eof() ++ ++ msg2, payload2 = messages[1] ++ assert msg2.method == "POST" ++ assert msg2.path == "/next" ++ assert payload2.is_eof() ++ assert not upgraded ++ assert not tail ++ ++ ++def test_http_request_chunked_payload_chunks(parser: HttpRequestParser) -> None: + text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n" + msg, payload = parser.feed_data(text)[0][0] + +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index 917405c715..40ac847c01 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -24,6 +24,7 @@ SRC_URI += " \ file://CVE-2026-69244.patch \ file://CVE-2026-111066_p1.patch \ file://CVE-2026-111066_p2.patch \ + file://CVE-2026-111750.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:26:58 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103232 Received: from rcdn-iport-5.cisco.com (rcdn-iport-5.cisco.com [173.37.86.76]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1306.1793950037587061845 for ; Thu, 05 Nov 2026 23:27:17 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=N//60w/6; spf=pass (domain: cisco.com, ip: 173.37.86.76, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=10105; q=dns/txt; s=iport01; t=1793950037; x=1795159637; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=IIVUTOiZeuf78/d1W9z8KlQ5ybKkjIZ2CnNIdHfjztc=; b=N//60w/6sVGnGYApLu031MzC+0TmFpk3FzZvyenZjL6j6KvtDiY9wgsx 9inGb+9rp5YpJr9uRgTcN0YHr07KVzr5FQGBzyRQvzlWTSe8LqbK4P0Aa 80TkweKReWjbeGNcABStFLtgZX1OrnsXZCQ/ZN1HjlGwUjjx1LVPDUzst DChbfaQpBNTnKDFvSRwxJHhgPxbJ7zZ8QHVVthw4m9Ez7ZOdJHYPdWPNh mv8YbhGy0FBLnEb2miYQ1H7s7MBouXtZpSjS8kGxC1V/gMsZCuqI+0PVF LASOT3gY7VtnQpkQwZkrCWgfIsHTzPmMbvAof/nDpV6c/0WOZOdWl9Tuy A==; X-CSE-ConnectionGUID: SnwDtqM2SQCLuI+AlHe8cg== X-CSE-MsgGUID: MR3Zu+srRFSNGMwPd5PYgA== X-IPAS-Result: A0BJAgDmf+1q/43/Ja1RCR4BAQsSDIIFC4JXdWBDSQOWSAORTIxVgX4PAQEBD0QNBAEBhD9GAo4dAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQEKAQEFAQEBAgEHBYEOE4ZPDZASAQIBAycLARgBLRAcAwECLysjCBmDAgGCdAMRvBKBeTOBAYMpAYFU2zMBCxQBgTiFQIgjXRgBhH0nGxuBcoEVgTuCLoEFgVwBAYE0hnIEgiKBDIFagQMJjAwCgVGGeUiBHgNZLAFVEw0KCwcFgWgDNRIqFW4yHYEjPheBBxsGBYEdgR6BEoEHXYErKF0BIYEIgXYCglGCAAIBSUEOB0VTCSVDEkckIggSCQETGiwKgRo1PAkmPxUMZAsaBWEIAg0MdDkQMB4kLwUcJj4bCi8YDUgRLDcVGQQ+bgeSPRWCdgctVA0BKgGBfRAfHB6jG4IhoREKKIN4jCSVOhozhASBV6UWC5h9glmCeoMugjaCU5VXKgNNhGmBaDyBWXAVO4JnCUoZD1iNVgsLg2CFZMZNJjICCQMvAQEHAgcOAwuBaJF+AQE IronPort-Data: A9a23:P8c3S6DccA8XJRVW/3niw5YqxClBgxIJ4kV8jS/XYbTApGtw0GYCz GMaUDzQb6qIZWL3fNp3aNnk8UNVvZbcx9NgOVdlrnsFo1CmBibm6XV1Cm+qYkt+++WaFBoPA /02M4eGcYZsCCCM/n9BC5C5xVFkz6aEW7HgP+DNPyF1VGdMRTwo4f5Zs7ZRbrVA357jXGthh fuo+5eBYQb9hmYtWo4pw/vrRC1H7ayaVAww5jTSVdgT1HfCmn8cCo4oJK3ZBxPQXolOE+emc P3Ixbe/83mx109F5gSNy+uTnuUiG9Y+DCDW4pZkc/HKbitq+kTe5p0G2M80Mi+7vdkmc+dZk 72hvbToIesg0zaldO41C3G0GAkmVUFKFSOuzXWX6aSuI0P6n3TE+PNxIF0vGNcjw81THEAN3 Pg4JWAGYUXW7w626OrTpuhEnM8vKozveYgYoHwllW2fBvc9SpeFSKLPjTNa9G5v3YYVQrCEO pdfMGY2BPjDS0Un1lM/BJEzmO6pl3DXeDxDo1XTrq0yi4TW5FEtjeK9YIaMILRmQ+1wpB2qu yHd5V7XOTc1EcOnxWe9w32F07qncSTTHdh6+KeD3vlyjVuew2YeBBEbWR63rOe0jma6WslDM AoT4icooK04+UCnQ9W7WAe3yENopTYGUNZWVul/4waXx++Mv0CSB3MPSXhKb9lOWNIKeAHGH 2Shx7vBbQGDepXPIZ5B3t94dQ+PBBU= IronPort-HdrOrdr: A9a23:6MEpwausRpRxmbQ6WxTCofhm7skDVNV00zEX/kB9WHVpmwKj+P xG+85rsiMc5wxxZJhNo7290ey7MBHhHP1OkO0s1MmZPDUO0VHAROoJ0WKh+UyEJ8SUzIBgPM lbH5SWcOeAbmRSvILd/BSyFcomzZ2s9aClgvqb8lJWJDsaDZ2JK2xCe36m+oocfng+OaYE X-Talos-CUID: 9a23:zlSLWm9AFiKefSIIZkaVv3USBew5a1iH9077B1KZSk9taLDFQlDFrQ== X-Talos-MUID: 9a23:6PfhFwjGtcFYxAIzyOgEF8MpEf0z0bntE1o3v64glOybDSAtAwaSpWHi X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="545288412" Received: from rcdn-l-core-04.cisco.com ([173.37.255.141]) by rcdn-iport-5.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:16 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-04.cisco.com (Postfix) with ESMTPS id 7F2A618000191; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 2B656CBF203; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 3/7] python3-aiohttp: fix CVE-2026-111749 Date: Thu, 5 Nov 2026 23:26:58 -0800 Message-Id: <20261106072702.502813-4-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-04.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:21 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131966 From: Darsh Kelaiya Backport the upstream host-only cookie state fix [1] to aiohttp 3.13.5, as identified by [2]. Track host-only state by domain, path and name so expiring another cookie cannot widen its domain scope. Preserve this identity during filtering, replacement and JSON persistence. Adapt regression assertions to the existing private state because 3.13.5 lacks the newer public inspection property. [1] https://github.com/aio-libs/aiohttp/commit/6a07c171ef9f3be583e2907dcd1931372efca8d6 [2] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-gx76-c99x-2c96 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111749.patch | 205 ++++++++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 1 + 2 files changed, 206 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111749.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111749.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111749.patch new file mode 100644 index 0000000000..475229e180 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111749.patch @@ -0,0 +1,205 @@ +From 6a07c171ef9f3be583e2907dcd1931372efca8d6 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Wed, 9 Sep 2026 02:21:06 +0100 +Subject: [PATCH] Fix host-only state on cookie expiration (#13674) (#13676) + +(cherry picked from commit 9332972cfcb0f5a2b6677e7307819ab8f13b5d09) + +CVE: CVE-2026-111749 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/6a07c171ef9f3be583e2907dcd1931372efca8d6] + +Backport Changes: +- Refresh the runtime hunks for aiohttp 3.13.5 and its existing + JSON cookie persistence backport; retain Set/Tuple type spelling. +- Omit annotation changes for the public host_only_cookies property + in CookieJar, DummyCookieJar, AbstractCookieJar and the test jar. + That property and its documentation were introduced in 3.14. +- Omit the absent public-property test update and use private + _host_only_cookies assertions in the regression and JSON tests. + Adapt both existing JSON scope assertions to the private state. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13674.bugfix.rst | 1 + + aiohttp/cookiejar.py | 25 +++++++++------- + tests/test_cookiejar.py | 64 ++++++++++++++++++++++++++++++++++++++-- + 3 files changed, 77 insertions(+), 13 deletions(-) + create mode 100644 CHANGES/13674.bugfix.rst + +diff --git a/CHANGES/13674.bugfix.rst b/CHANGES/13674.bugfix.rst +new file mode 100644 +index 0000000..5152bce +--- /dev/null ++++ b/CHANGES/13674.bugfix.rst +@@ -0,0 +1 @@ ++Fixed host-only cookie state being lost on expiration -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index b07d1fd..c5e5b09 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -139,7 +139,8 @@ class CookieJar(AbstractCookieJar): + self._morsel_cache: DefaultDict[Tuple[str, str], Dict[str, Morsel[str]]] = ( + defaultdict(dict) + ) +- self._host_only_cookies: Set[Tuple[str, str]] = set() ++ # Cookie identity is (domain, path, name). ++ self._host_only_cookies: Set[Tuple[str, str, str]] = set() + self._unsafe = unsafe + self._quote_cookie = quote_cookie + if treat_as_secure_origin is None: +@@ -186,7 +187,7 @@ class CookieJar(AbstractCookieJar): + if attr_val: + morsel_data[attr] = attr_val + # Persist or it reloads as a domain cookie and leaks to subdomains. +- if (domain, name) in self._host_only_cookies: ++ if (domain, path, name) in self._host_only_cookies: + morsel_data["host_only"] = True + if (exp := self._expirations.get((domain, path, name))) is not None: + morsel_data["expires_timestamp"] = exp +@@ -340,7 +341,7 @@ class CookieJar(AbstractCookieJar): + + def _delete_cookies(self, to_del: List[Tuple[str, str, str]]) -> None: + for domain, path, name in to_del: +- self._host_only_cookies.discard((domain, name)) ++ self._host_only_cookies.discard((domain, path, name)) + self._cookies[(domain, path)].pop(name, None) + self._morsel_cache[(domain, path)].pop(name, None) + self._expirations.pop((domain, path, name), None) +@@ -377,18 +378,12 @@ class CookieJar(AbstractCookieJar): + domain = "" + del cookie["domain"] + +- if not domain and hostname is not None: +- # Set the cookie's domain to the response hostname +- # and set its host-only-flag +- self._host_only_cookies.add((hostname, name)) +- domain = cookie["domain"] = hostname +- + if domain and domain[0] == ".": + # Remove leading dot + domain = domain[1:] + cookie["domain"] = domain + +- if hostname and not self._is_domain_match(domain, hostname): ++ if domain and hostname and not self._is_domain_match(domain, hostname): + # Setting cookies for different domains is not allowed + continue + +@@ -404,6 +399,14 @@ class CookieJar(AbstractCookieJar): + cookie["path"] = path + path = path.rstrip("/") + ++ if not domain and hostname is not None: ++ self._host_only_cookies.add((hostname, path, name)) ++ domain = cookie["domain"] = hostname ++ else: ++ # A cookie with an explicit Domain attribute replaces any ++ # host-only cookie with the same (domain, path, name) identity. ++ self._host_only_cookies.discard((domain, path, name)) ++ + if max_age := cookie["max-age"]: + try: + delta_seconds = int(max_age) +@@ -493,7 +496,7 @@ class CookieJar(AbstractCookieJar): + for name, cookie in self._cookies[p].items(): + domain = cookie["domain"] + +- if (domain, name) in self._host_only_cookies and domain != hostname: ++ if domain != hostname and p + (name,) in self._host_only_cookies: + continue + + # Skip edge case when the cookie has a trailing slash but request doesn't. +diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py +index ba7ee32..f82861e 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -1625,6 +1625,44 @@ async def test_shared_cookie_with_multiple_domains() -> None: + assert "universal" in jar._morsel_cache[("", "")] + + ++async def test_host_only_marker_survives_same_name_expiry_on_other_path() -> None: ++ """Expiring a same-name cookie on another path must not clear host-only state.""" ++ jar = CookieJar() ++ origin = URL("http://auth.example.com/") ++ subdomain = URL("http://evil.auth.example.com/") ++ ++ jar.update_cookies_from_headers(["sid=secret; Path=/"], origin) ++ assert "sid" not in jar.filter_cookies(subdomain) ++ ++ # Attacker-controlled descendant expires a same-name cookie on its own path. ++ jar.update_cookies_from_headers( ++ ["sid=gone; Domain=auth.example.com; Path=/attacker; Max-Age=0"], ++ subdomain, ++ ) ++ ++ assert ("auth.example.com", "", "sid") in jar._host_only_cookies ++ assert "sid" not in jar.filter_cookies(subdomain) ++ assert jar.filter_cookies(origin)["sid"].value == "secret" ++ ++ ++async def test_explicit_domain_replacement_clears_host_only_marker() -> None: ++ """A replacing cookie with an explicit Domain is a domain cookie.""" ++ jar = CookieJar() ++ origin = URL("http://example.com/") ++ subdomain = URL("http://sub.example.com/") ++ ++ jar.update_cookies_from_headers(["sid=hostonly; Path=/"], origin) ++ assert ("example.com", "", "sid") in jar._host_only_cookies ++ assert "sid" not in jar.filter_cookies(subdomain) ++ ++ jar.update_cookies_from_headers( ++ ["sid=domainwide; Domain=example.com; Path=/"], origin ++ ) ++ ++ assert jar._host_only_cookies == set() ++ assert jar.filter_cookies(subdomain)["sid"].value == "domainwide" ++ ++ + # === Security tests for restricted unpickler and JSON save/load === + + +@@ -1775,7 +1813,7 @@ async def test_save_load_json_preserves_host_only_scope(tmp_path: Path) -> None: + jar_load = CookieJar() + jar_load.load(file_path=file_path) + +- assert jar_load.host_only_cookies == frozenset({("auth.example.com", "sid")}) ++ assert jar_load._host_only_cookies == {("auth.example.com", "", "sid")} + assert "sid" not in jar_load.filter_cookies(subdomain) + assert "sid" in jar_load.filter_cookies(issuer) + +@@ -1796,10 +1834,32 @@ async def test_save_load_json_domain_cookie_still_matches_subdomain( + jar_load = CookieJar() + jar_load.load(file_path=file_path) + +- assert jar_load.host_only_cookies == frozenset() ++ assert jar_load._host_only_cookies == set() + assert "sid" in jar_load.filter_cookies(subdomain) + + ++async def test_save_load_json_host_only_per_path(tmp_path: Path) -> None: ++ """Verify save/load keeps host-only state per (domain, path, name).""" ++ file_path = tmp_path / "per_path.json" ++ origin = URL("https://example.com/") ++ subdomain = URL("https://sub.example.com/") ++ ++ jar_save = CookieJar() ++ jar_save.update_cookies_from_headers( ++ ["sid=hostonly; Path=/", "sid=domainwide; Domain=example.com; Path=/api"], ++ origin, ++ ) ++ jar_save.save(file_path=file_path) ++ ++ jar_load = CookieJar() ++ jar_load.load(file_path=file_path) ++ ++ assert jar_load._host_only_cookies == {("example.com", "", "sid")} ++ assert "sid" not in jar_load.filter_cookies(subdomain) ++ filtered = jar_load.filter_cookies(URL("https://sub.example.com/api/x")) ++ assert filtered["sid"].value == "domainwide" ++ ++ + async def test_save_load_json_preserves_max_age_deadline(tmp_path: Path) -> None: + """Verify save/load restores the absolute deadline without resetting it.""" + file_path = tmp_path / "max_age.json" +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index 40ac847c01..17398ab323 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -25,6 +25,7 @@ SRC_URI += " \ file://CVE-2026-111066_p1.patch \ file://CVE-2026-111066_p2.patch \ file://CVE-2026-111750.patch \ + file://CVE-2026-111749.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:26:59 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103231 Received: from rcdn-iport-1.cisco.com (rcdn-iport-1.cisco.com [173.37.86.72]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1305.1793950037539436545 for ; Thu, 05 Nov 2026 23:27:17 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=fr8hpXgT; spf=pass (domain: cisco.com, ip: 173.37.86.72, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=7991; q=dns/txt; s=iport01; t=1793950037; x=1795159637; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=CvgLTEh/jGMSxMOfEELYfJshAIso41F/h605AYXoV0c=; b=fr8hpXgTa90dNz4qZ8y3IEvIrR+nqOMYHANnFK+GwLBiQjLuS6QwMIOp V4tzdq+awtgjLKAFE1hINizSDEay1V/wtG/S4f1nSNRkypeRXxJQzzrpi kr/JO/66D97opAr4LQGUMCjvIeykBSUEbzzHyljXv1+Hun5P03lL4jXQW 0eQK9e23jwIG6iU2y4bF5NFaVoIzA8DfEO8nIt46DAeJv+JqpmVVCU6u4 NnJLSAiwyubN700XynaKurFi4L5hYbb8CYtRWR3Z4fZ38LD8qgjbaG0eX ueR9BpUWnZlta4PF8JsILZsyhdMBaxXN8GL8wk/U07UsnHn0HukJO+Jg0 Q==; X-CSE-ConnectionGUID: lRmkQrGATxa0RLIRTMbT+w== X-CSE-MsgGUID: 034ESAt2St+Go0rqDfN5EQ== X-IPAS-Result: A0BJAgBpgO1q/5X/Ja1aglmCGD91YENJA5ZIA54hgX4PAQEBD0QNBAEBhD9GAo4dAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQEKAQEFAQEBAgEHBYEOE4ZPDZASAQIBAycLARgBLRAcAwECLysjCBmCKlgBgnQDEbwTgXkzgQGDKQFDQVDbMwELFAGBOIVAiCNdGAGEfScbG4FygRWBO4IugQWBXAEBAgGBJBGGbgSCIoEMgVqBA4wVAoFRhnlIgR4DWSwBVRMNCgsHBYFoAzUSKhVuMh2BIz4XgQcbBgWBHYEegRKBB12BKyhdASGBCIF2AoJRggACAUlBDgdFUwklQxJHJCIIEgkBExosCoEaNTwJJj8VDGQLGgVhCAINDHQ5EDAeJC8FHCY+GwovGA1IESw3FRkEPm4Hkj2DAhAOdgoBExIEAn9VUwUcAjCSZwOQHYIhoREKKIN4jCSVOhozhVulFguYfYkBgjaCU5YBUIRpgWg8gVlwFTuCZwlKGQ+OLgsLgRQBAoJJhRNRxk0mMgIBCAMvAQEHAgcOAwuBaJAAASeBVgEB IronPort-Data: A9a23:DXoDtqtTH27I9JK2Fm+SuiZdQefnVAZfMUV32f8akzHdYApBsoF/q tZmKW2FafrZYWqme9klbdi/9k8DsJDVx4NhSAdkpXhkQX4SgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrb/656yYsjclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZzdJ5xYuajhKsvvb9Us21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIw/txyAlhU2 tUkODlVVDSivd6Tyo74Vbw57igjBJGD0II3oHpsy3TdSP0hW52GGv+M7t5D1zB2jcdLdRrcT 5NGMnw0M1KaPkAJYwtGYH49tL/Aan3Xfz5VrFuUtKMf6GnIxws327/oWDbQUoHWGZoKzh7J/ Aoq+UyhWU03D/WDkwGZsV2r3vXWsS3dBa8rQejQGvlCxQf7KnYoIBoOWF22pPO0hkKzV5dTJ lIZ/gIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sYMpottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvrUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:xw/v7KFxAmCzVx6MpLqEyseALOsnbusQ8zAXPo5KJiC9Ffbo8P xG88576faZslsssTQb6LK90cq7MBfhHOBOgbX5VI3KNGKNhILrFvAG0WKI+VPd8kPFmtK1rZ 0QEJRWOZnXEUVwi9r87U2TFtYtx8TCzYWT7N2uqUuEiWpRGtldB8ATMHfjLnFL X-Talos-CUID: 9a23:tXU8c2hP+hfyWo3XkdqDThF7PzJudkfYz07KDGaEGW8xcpm0c2O18p8njJ87 X-Talos-MUID: 9a23:wEWJGQoIAh+GyeLTo6Eez2k5aOpTyrj0M1xOvpJeodCoOwotFg7I2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="544778092" Received: from rcdn-l-core-12.cisco.com ([173.37.255.149]) by rcdn-iport-1.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:16 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-12.cisco.com (Postfix) with ESMTPS id 83B06180001C9; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 2FF36CBF204; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 4/7] python3-aiohttp: fix CVE-2026-111065 Date: Thu, 5 Nov 2026 23:26:59 -0800 Message-Id: <20261106072702.502813-5-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-12.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:20 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131965 From: Darsh Kelaiya Backport the upstream cookie overflow and response cleanup fix [1] to aiohttp 3.13.5, as identified by [2]. Clamp positive Max-Age values before float conversion and expire nonpositive values immediately. Close a received response if subsequent processing fails so a malformed cookie cannot exhaust the connection pool. Include the upstream extreme Max-Age and connection-release regression tests. Adapt the response annotation and imports to the existing code. [1] https://github.com/aio-libs/aiohttp/commit/9dd8eaf4e31f1237ef2c1574a8c82a739fca5805 [2] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-6cjc-53gc-5rqm Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111065.patch | 176 ++++++++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 1 + 2 files changed, 177 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111065.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111065.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111065.patch new file mode 100644 index 0000000000..5ee6a44dff --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111065.patch @@ -0,0 +1,176 @@ +From 9dd8eaf4e31f1237ef2c1574a8c82a739fca5805 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Wed, 9 Sep 2026 14:43:30 +0100 +Subject: [PATCH] [PR #13677/9e08ba02 backport][3.14] Fix cookie overflow + (#13679) + +**This is a backport of PR #13677 as merged into master +(9e08ba02abe573ce9e4cc541d4e3c3646adb43a7).** + +--------- + +Co-authored-by: Sam Bull + +CVE: CVE-2026-111065 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/9dd8eaf4e31f1237ef2c1574a8c82a739fca5805] + +Backport Changes: +- Use the existing Optional type spelling for the response variable. +- Add Sequence to the existing typing imports for the regression test. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13677.bugfix.rst | 1 + + aiohttp/client.py | 5 ++++ + aiohttp/cookiejar.py | 10 +++++++- + tests/test_client_functional.py | 45 +++++++++++++++++++++++++++++++++ + tests/test_cookiejar.py | 22 ++++++++++++++++ + 5 files changed, 82 insertions(+), 1 deletion(-) + create mode 100644 CHANGES/13677.bugfix.rst + +diff --git a/CHANGES/13677.bugfix.rst b/CHANGES/13677.bugfix.rst +new file mode 100644 +index 0000000..bea67cb +--- /dev/null ++++ b/CHANGES/13677.bugfix.rst +@@ -0,0 +1 @@ ++Fixed a possible ``OverflowError`` on cookies and a connection not being closed properly -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/client.py b/aiohttp/client.py +index 8cd9add..f404b55 100644 +--- a/aiohttp/client.py ++++ b/aiohttp/client.py +@@ -628,6 +628,7 @@ class ClientSession: + await trace.send_request_start(method, url.update_query(params), headers) + + timer = tm.timer() ++ resp: Optional[ClientResponse] = None + try: + with timer: + # https://www.rfc-editor.org/rfc/rfc9112.html#name-retrying-requests +@@ -939,6 +940,10 @@ class ClientSession: + handle.cancel() + handle = None + ++ if resp is not None: ++ # A failure occurred after the response was received. ++ resp.close() ++ + for trace in traces: + await trace.send_request_exception( + method, url.update_query(params), headers, e +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index c5e5b09..6712b90 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -410,7 +410,15 @@ class CookieJar(AbstractCookieJar): + if max_age := cookie["max-age"]: + try: + delta_seconds = int(max_age) +- max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME) ++ # https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 ++ if delta_seconds <= 0: ++ max_age_expiration = 0.0 ++ else: ++ # Cap first to protect against OverflowError on next line. ++ delta_seconds = min(delta_seconds, self.MAX_TIME) ++ max_age_expiration = min( ++ time.time() + delta_seconds, self.MAX_TIME ++ ) + self._expire_cookie(max_age_expiration, domain, path, name) + except ValueError: + cookie["max-age"] = "" +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 60caf6e..3415b83 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -23,6 +23,7 @@ from typing import ( + List, + NoReturn, + Optional, ++ Sequence, + Type, + ) + from unittest import mock +@@ -2978,6 +2979,50 @@ async def test_set_cookies_max_age_overflow(aiohttp_client) -> None: + assert int(cookie["max-age"]) == int(overflow) + + ++async def test_connection_released_when_cookie_processing_fails( ++ aiohttp_client: AiohttpClient, ++) -> None: ++ class EvilJar(aiohttp.CookieJar): ++ def update_cookies_from_headers( ++ self, headers: Sequence[str], response_url: URL ++ ) -> None: ++ raise RuntimeError("boom") ++ ++ hold = asyncio.Event() ++ ++ async def hostile(request: web.Request) -> web.StreamResponse: ++ ret = web.StreamResponse() ++ ret.content_length = 2 ++ ret.set_cookie("sid", "x") ++ await ret.prepare(request) ++ await ret.write(b"x") ++ await hold.wait() ++ assert False ++ ++ async def clean(request: web.Request) -> web.Response: ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/hostile", hostile) ++ app.router.add_get("/clean", clean) ++ connector = aiohttp.TCPConnector(limit=1) ++ client = await aiohttp_client(app, connector=connector, cookie_jar=EvilJar()) ++ ++ try: ++ for _ in range(2): ++ with pytest.raises(RuntimeError, match="boom") as excinfo: ++ await client.get("/hostile") ++ assert not connector._acquired ++ del excinfo ++ ++ # The single connector slot is free again: an unaffected request ++ # succeeds instead of waiting forever for a connection. ++ async with client.get("/clean") as resp: ++ assert resp.status == 200 ++ finally: ++ hold.set() ++ ++ + async def test_request_conn_error() -> None: + client = aiohttp.ClientSession() + with pytest.raises(aiohttp.ClientConnectionError): +diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py +index 5779df9..f3719a7 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -2005,3 +2005,25 @@ async def test_save_load_json_secure_cookies(tmp_path: Path) -> None: + assert cookie["secure"] is True + assert cookie["httponly"] is True + assert cookie["domain"] == "example.com" ++ ++ ++async def test_update_cookies_max_age_beyond_float_range_is_clamped() -> None: ++ """A hostile Max-Age larger than float max must clamp, not raise OverflowError.""" ++ url = URL("https://example.com/") ++ jar = CookieJar() ++ ++ jar.update_cookies_from_headers([f"sid=x; Max-Age={'9' * 309}"], url) ++ ++ assert "sid" in jar.filter_cookies(url) ++ assert jar._expirations[("example.com", "", "sid")] == CookieJar.MAX_TIME ++ ++ ++async def test_update_cookies_negative_max_age_beyond_float_range_expires() -> None: ++ """A negative Max-Age below float min must expire the cookie, not raise.""" ++ url = URL("https://example.com/") ++ jar = CookieJar() ++ ++ jar.update_cookies_from_headers([f"sid=x; Max-Age=-{'9' * 309}"], url) ++ ++ assert "sid" not in jar.filter_cookies(url) ++ assert len(jar) == 0 +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index 17398ab323..e2ac1eda3a 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -26,6 +26,7 @@ SRC_URI += " \ file://CVE-2026-111066_p2.patch \ file://CVE-2026-111750.patch \ file://CVE-2026-111749.patch \ + file://CVE-2026-111065.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:27:00 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103229 Received: from rcdn-iport-5.cisco.com (rcdn-iport-5.cisco.com [173.37.86.76]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1306.1793950037587061845 for ; Thu, 05 Nov 2026 23:27:18 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=I2bm2JRb; spf=pass (domain: cisco.com, ip: 173.37.86.76, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=27971; q=dns/txt; s=iport01; t=1793950038; x=1795159638; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=WWm1O8K3svVhDejNjVum5bCVpEdLmI7qHbAJf7+u4U0=; b=I2bm2JRbCptvCZBdY4MNE4ldlW2elOae7VJFKFK1Y+9Hf/KSP6ZVlzQA Fbdh1cMyVPvo0ZP+9xE9IAp/Q47ZT0Oeu3BPvPOUW0fq74xseEtdCxFcp vzDlX/4RoHEdVrGDRu3VaQeDx5FgRr9RPo7WdsjUJupiYFgBQ76sUQRxS Q34qIvoNaRXokAFM0eXizeQhRVyzj8st4uXg03ajqGbXYaATHU5RHtWvS COB1VdfVFS2KlXDab6b4udSGqpYo60UIkv9IPl3cEJw/wpa2FOuZeCnym AaHbaT65XEpNJp6rnP56a7wxM4usIweuu5IEf1WkD7blYn8QTA0BDjahf A==; X-CSE-ConnectionGUID: ggsB5dbpQUGCAvIcfm87pw== X-CSE-MsgGUID: HDVk3HYdTIuRouuv6H0p/Q== X-IPAS-Result: A0AbAADmf+1q/4//Ja1aHAEBAQEBAQcBARIBAQQEAQFMgTAHAQELAYJWdWBDSQOEVIgbiVkDgROKUoVnjFUUgRADVw8BAQEPRA0EAQGEMA9GAk8VjTkCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDGgkECwEWAgEtEBwDAQIDAiYCAiALGgkIEAkagmgBgjoDNwMRvBJ6fzOBAYMpAUNBUNhMDYJaAQsUAYEKLgGFP4J/IAGFA10YAYR9JxsbgXKBFYE7gTh2gQVLAQFNQgEBAQEBF4EsAoN0gmoEgg0VgQyBWn4FCYIZgxgDhlgCgVGDfIJ9SIECHANZLAFVEw0KCwcFgWgDNRIqFW4yHYEjPhcvWBsGBYEdgR6BEoEHXYErKF0BIYEIgXYCglGCAAIBSUEOB0VTCSVDEkckIggSCQETGiwKgRo1PAkmPxUMZAsaBWEIAg0MdDkQMB4kLwUcJj4bCi8YDUgRLDcVGQQ9AW4Hkj2DCwYBAWAtAQcDEQ4BARcJAjsoDA8UGQgFHg0DCwETBQYJAgsCBCySVxACATkKj1qCIYE1nh5NcQoog3iMJI8+hXwaM4QEgVeLPQOHApJUC5h9glmCeoVkglOECpFUCwsNA02EaYFoPDmBIHAVO4JnCUoZD44rDguDYII2gl3HHiYyAgEIAy8BAQcCBw4DC4FokAItgU8BAQ IronPort-Data: A9a23:UK+YCqtidOTQoVsIYC76fWIGEufnVAZfMUV32f8akzHdYApBsoF/q tZmKWuObqmCZWD8ft5yYITn9U1Vv8XQyIBiHlM++SE2Ri5AgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrb/656yYsjclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZzdJ5xYuajhKsvvb9Us21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIw9elsKzxC/ +QhFR8mRw6ijOyzkO2jVbw57igjBJGD0II3oHpsy3TdSP0hW52GG/uM7t5D1zB2jcdLdRrcT 5NGMnw0M1KaPkAJYwtMYH49tL/Aan3Xfz5VrFuUtKMf6GnIxws327/oWDbQUoHWGJ8FzhrB+ woq+UzZLi8Zb/yuxQak70irm/LRwwOjQMUrQejQGvlCxQf7KnYoIBoOWF22pPO0hkKzV5dTJ lIZ/gIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sZNpottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvpUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:wkAn3aqgR1B6TChyppeUEsAaV5rzeYIsimQD101hICG9vPb2qy nIpoV86faUskd3ZJhOo7G90cW7LE80sKQFg7X5Xo3SODUOxlHJEGgK1+KLqFfd8m/Fh4tgPM xbHZSWZuedMbEDt7eY3CCIV/A93dKA7Kekwc3az3trUEVWTpsI1XYBNu5eeXcGPzWvwvECZe Kh2vY= X-Talos-CUID: 9a23:/RWrEWjmHxAAWOFbQ1a6GE4spDJufXLG5ircKVeBNElCYaO0QlGVqPx8jJ87 X-Talos-MUID: 9a23:rT09rAxbcXBmUTAFzHZI+BPVIVuaqISCNkNWsaQKh+iZDyttJinDrGyvBbZyfw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="545288415" Received: from rcdn-l-core-06.cisco.com ([173.37.255.143]) by rcdn-iport-5.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:17 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-06.cisco.com (Postfix) with ESMTPS id D9A6F1800026D; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 34A5ECBF21E; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 5/7] python3-aiohttp: fix CVE-2026-111748 Date: Thu, 5 Nov 2026 23:27:00 -0800 Message-Id: <20261106072702.502813-6-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-06.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:20 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131970 From: Darsh Kelaiya Backport the protocol-scheme fix [1] to aiohttp 3.13.5, as identified by [3]. Derive Request.scheme from the transport rather than the untrusted scheme in an absolute-form request target. Include the authority-validation correction [2] to reject malformed targets before URL parsing, independently of yarl normalization. Retain the upstream parser and request regressions, adapted to the existing transport-test interface. The recipe regenerates the C parser from the patched Cython source. [1] https://github.com/aio-libs/aiohttp/commit/6d4e7b01f9b951aa084a75d7356e907d4ee80411 [2] https://github.com/aio-libs/aiohttp/commit/186065caf37d5999e91fd3d4ddfd03b43eb08801 [3] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-f9xw-jh8m-3mwq Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111748_p1.patch | 376 ++++++++++++++++++ .../python3-aiohttp/CVE-2026-111748_p2.patch | 282 +++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 2 + 3 files changed, 660 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p1.patch new file mode 100644 index 0000000000..30dd8e2b48 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p1.patch @@ -0,0 +1,376 @@ +From 6d4e7b01f9b951aa084a75d7356e907d4ee80411 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Sun, 27 Sep 2026 18:11:00 +0100 +Subject: [PATCH] Use protocol scheme for absolute-form requests (#13821) + (#13827) (#13828) + +(cherry picked from commit a921cfd0fec28d5a9e5bf8d6f4372b8fa63c49df) +(cherry picked from commit 93d91e37e7c6035d09e89646b3de737735ed91b5) + +CVE: CVE-2026-111748 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/6d4e7b01f9b951aa084a75d7356e907d4ee80411] + +Backport Changes: +- Omit the enum declaration hunk: HTTP_OPTIONS is already declared. +- Adapt the request-test imports and constructor helper to 3.13.5; + omit newer transport mock attributes absent from RequestHandler. +- Retain the existing surrounding parser-test type annotation. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13821.bugfix.rst | 1 + + aiohttp/_http_parser.pyx | 29 +++++++++++++++--- + aiohttp/http_parser.py | 26 +++++++++++++--- + aiohttp/web_request.py | 7 ++++- + tests/test_http_parser.py | 59 ++++++++++++++++++++++++++++++++++++ + tests/test_web_functional.py | 46 ++++++++++++++++++++++++++-- + tests/test_web_request.py | 43 +++++++++++++++++++------- + 7 files changed, 189 insertions(+), 22 deletions(-) + create mode 100644 CHANGES/13821.bugfix.rst + +diff --git a/CHANGES/13821.bugfix.rst b/CHANGES/13821.bugfix.rst +new file mode 100644 +index 0000000..f35418d +--- /dev/null ++++ b/CHANGES/13821.bugfix.rst +@@ -0,0 +1 @@ ++Fixed the web server trusting the scheme of an absolute-form request-target -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index ffbd53f..4390253 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -669,12 +669,20 @@ cdef class HttpRequestParser(HttpParser): + return + self._path = self._buf.decode('utf-8', 'surrogateescape') + try: +- idx3 = len(self._path) + if self._cparser.method == cparser.HTTP_CONNECT: + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 +- self._url = URL.build(authority=self._path, encoded=True) +- elif idx3 > 1 and self._path[0] == '/': ++ try: ++ self._url = URL.build(authority=self._path, encoded=True) ++ host = self._url.raw_host ++ except ValueError: ++ host = None ++ if host is None: ++ raise InvalidURLError( ++ self._path.encode(errors="surrogateescape") ++ .decode("latin1") ++ ) ++ elif self._path[0] == '/': + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 + idx1 = self._path.find("?") +@@ -705,10 +713,23 @@ cdef class HttpRequestParser(HttpParser): + fragment=fragment, + encoded=True, + ) ++ elif self._path == '*' and self._cparser.method == cparser.HTTP_OPTIONS: ++ # asterisk-form, ++ self._url = URL(self._path, encoded=True) + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 +- self._url = URL(self._path, encoded=True) ++ try: ++ self._url = URL(self._path, encoded=True) ++ host = self._url.raw_host ++ except ValueError: ++ host = None ++ # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4 ++ if host is None: ++ raise InvalidURLError( ++ self._path.encode(errors="surrogateescape") ++ .decode("latin1") ++ ) + finally: + PyByteArray_Resize(self._buf, 0) + +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index ce15764..1c0650a 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -665,7 +665,18 @@ class HttpRequestParser(HttpParser[RawRequestMessage]): + if method == "CONNECT": + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 +- url = URL.build(authority=path, encoded=True) ++ try: ++ url = URL.build(authority=path, encoded=True) ++ host = url.raw_host ++ except ValueError: ++ # Duplicated so mypy understands that url must be defined below. ++ raise InvalidURLError( ++ path.encode(errors="surrogateescape").decode("latin1") ++ ) ++ if host is None: ++ raise InvalidURLError( ++ path.encode(errors="surrogateescape").decode("latin1") ++ ) + elif path.startswith("/"): + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 +@@ -688,9 +699,16 @@ class HttpRequestParser(HttpParser[RawRequestMessage]): + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 +- url = URL(path, encoded=True) +- if url.scheme == "": +- # not absolute-form ++ try: ++ url = URL(path, encoded=True) ++ host = url.raw_host ++ except ValueError: ++ # Duplicated so mypy understands that url must be defined below. ++ raise InvalidURLError( ++ path.encode(errors="surrogateescape").decode("latin1") ++ ) ++ # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4 ++ if host is None: + raise InvalidURLError( + path.encode(errors="surrogateescape").decode("latin1") + ) +diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py +index e978b2a..6e9de9a 100644 +--- a/aiohttp/web_request.py ++++ b/aiohttp/web_request.py +@@ -207,8 +207,14 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + self._cache: Dict[str, Any] = {} + url = message.url + if url.absolute: ++ if scheme is None and url.scheme: ++ # Absolute URL is peer-controlled, use the protocol. ++ scheme = "https" if protocol.ssl_context else "http" + if scheme is not None: ++ # Authority-form (CONNECT) has no scheme: leave the target ++ # unchanged unless clone(scheme=...) overrides it. + url = url.with_scheme(scheme) ++ self._cache["scheme"] = scheme + if host is not None: + url = url.with_host(host) + # absolute URL is given, +@@ -216,7 +222,6 @@ class BaseRequest(MutableMapping[str, Any], HeadersMixin): + # all other properties should be good + self._cache["url"] = url + self._cache["host"] = url.host +- self._cache["scheme"] = url.scheme + self._rel_url = url.relative() + else: + self._rel_url = url +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 4255f4d..610b77e 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -900,6 +900,65 @@ def test_url_absolute(parser: Any) -> None: + assert msg.url == URL("https://www.google.com/path/to.html") + + ++def test_url_authority_form_only_connect(parser: HttpRequestParser) -> None: ++ # https://www.rfc-editor.org/info/rfc9112/#section-3.2.3-1 ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"GET www.google.com:443 HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ ++@pytest.mark.parametrize( ++ "target", ++ ( ++ b"https:///protected", ++ b"https:////protected", ++ b"https://:80/protected", ++ b"https://user@/protected", ++ ), ++ ids=("empty-host", "empty-host-extra-slash", "port-only", "userinfo-only"), ++) ++def test_url_absolute_form_empty_host_rejected( ++ parser: HttpRequestParser, target: bytes ++) -> None: ++ # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.2-4 ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"GET " + target + b" HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ ++def test_url_absolute_form_invalid_port_rejected(parser: HttpRequestParser) -> None: ++ # yarl raises ValueError for an out-of-range port; that must surface as ++ # a 400, not escape the parser as a bare ValueError. ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"GET http://example.com:65536/x HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ ++def test_url_connect_invalid_port_rejected(parser: HttpRequestParser) -> None: ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"CONNECT example.com:65536 HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ ++def test_url_connect_empty_host_rejected(parser: HttpRequestParser) -> None: ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"CONNECT :80 HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ ++def test_url_origin_form_bare_slash(parser: HttpRequestParser) -> None: ++ messages, upgrade, tail = parser.feed_data(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n") ++ assert messages[0][0].url == URL("/") ++ ++ ++def test_url_asterisk_form_options(parser: HttpRequestParser) -> None: ++ # https://www.rfc-editor.org/rfc/rfc9112#section-3.2.4 ++ messages, upgrade, tail = parser.feed_data(b"OPTIONS * HTTP/1.1\r\nHost: a\r\n\r\n") ++ assert messages[0][0].url == URL("*") ++ ++ ++def test_url_asterisk_form_only_options(parser: HttpRequestParser) -> None: ++ # asterisk-form is only valid for OPTIONS; for other methods "*" is ++ # neither origin-form nor a valid absolute-form target. ++ with pytest.raises(http_exceptions.InvalidURLError): ++ parser.feed_data(b"GET * HTTP/1.1\r\nHost: a\r\n\r\n") ++ ++ + def test_headers_old_websocket_key1(parser: Any) -> None: + text = b"GET /test HTTP/1.1\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n" + +diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py +index 9fafd52..ffb2b95 100644 +--- a/tests/test_web_functional.py ++++ b/tests/test_web_functional.py +@@ -1929,8 +1929,50 @@ async def test_unread_compressed_body_drain_is_bounded( + assert max(drain_reads) < decompressed_size + + +-async def test_app_max_client_size(aiohttp_client) -> None: +- async def handler(request): ++async def test_absolute_form_target_does_not_spoof_scheme( ++ aiohttp_server: AiohttpServer, ++) -> None: ++ """A plaintext absolute-form target with https must not look secure.""" ++ ++ async def handler(request: web.Request) -> web.Response: ++ return web.json_response( ++ { ++ "scheme": request.scheme, ++ "secure": request.secure, ++ "url": str(request.url), ++ "host": request.host, ++ } ++ ) ++ ++ app = web.Application() ++ app.router.add_get("/probe", handler) ++ server = await aiohttp_server(app) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write( ++ b"GET https://trusted.example/probe HTTP/1.1\r\n" ++ b"Host: trusted.example\r\n" ++ b"Connection: close\r\n\r\n" ++ ) ++ await writer.drain() ++ raw = await asyncio.wait_for(reader.read(), 5) ++ finally: ++ writer.close() ++ with suppress(ConnectionResetError, BrokenPipeError): ++ await writer.wait_closed() ++ ++ assert raw.startswith(b"HTTP/1.1 200 ") ++ assert json.loads(raw.split(b"\r\n\r\n", 1)[1]) == { ++ "scheme": "http", ++ "secure": False, ++ "url": "http://trusted.example/probe", ++ "host": "trusted.example", ++ } ++ ++ ++async def test_app_max_client_size(aiohttp_client: AiohttpClient) -> None: ++ async def handler(request: web.Request) -> NoReturn: + await request.post() + return web.Response(body=b"ok") + +diff --git a/tests/test_web_request.py b/tests/test_web_request.py +index 7778128..44bf7a3 100644 +--- a/tests/test_web_request.py ++++ b/tests/test_web_request.py +@@ -11,13 +11,15 @@ import pytest + from multidict import CIMultiDict, CIMultiDictProxy, MultiDict + from yarl import URL + +-from aiohttp import HttpVersion ++from aiohttp import HttpVersion, web ++from aiohttp.abc import AbstractStreamWriter + from aiohttp.base_protocol import BaseProtocol + from aiohttp.http_exceptions import BadHttpMessage, LineTooLong + from aiohttp.http_parser import RawRequestMessage + from aiohttp.streams import StreamReader + from aiohttp.test_utils import make_mocked_request +-from aiohttp.web import BaseRequest, HTTPRequestEntityTooLarge ++from aiohttp.web import HTTPRequestEntityTooLarge ++from aiohttp.web_protocol import RequestHandler + from aiohttp.web_request import _FORWARDED_PAIR_RE, ETag + + +@@ -26,6 +28,19 @@ def protocol(): + return mock.Mock(_reading_paused=False) + + ++def make_base_request( ++ message: RawRequestMessage, protocol: RequestHandler ++) -> web.BaseRequest: ++ return web.BaseRequest( ++ message, ++ mock.create_autospec(StreamReader, spec_set=True, instance=True), ++ protocol, ++ mock.create_autospec(AbstractStreamWriter, spec_set=True, instance=True), ++ mock.create_autospec(asyncio.Task, spec_set=True, instance=True), ++ mock.create_autospec(asyncio.AbstractEventLoop, spec_set=True, instance=True), ++ ) ++ ++ + def test_base_ctor() -> None: + message = RawRequestMessage( + "GET", +@@ -40,9 +55,9 @@ def test_base_ctor() -> None: + URL("/path/to?a=1&b=2"), + ) + +- req = BaseRequest( +- message, mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() +- ) ++ protocol = mock.create_autospec(RequestHandler, spec_set=True, instance=True) ++ protocol.ssl_context = None ++ req = make_base_request(message, protocol) + + assert "GET" == req.method + assert HttpVersion(1, 1) == req.version +@@ -167,18 +182,24 @@ def test_non_ascii_raw_path() -> None: + + def test_absolute_url() -> None: + req = make_mocked_request("GET", "https://example.com/path/to?a=1") +- assert req.url == URL("https://example.com/path/to?a=1") +- assert req.scheme == "https" ++ assert req.url == URL("http://example.com/path/to?a=1") ++ # The scheme of an absolute-form target is peer-controlled and must not ++ # override the transport-derived scheme. ++ assert req.scheme == "http" ++ assert not req.secure + assert req.host == "example.com" + assert req.rel_url == URL.build(path="/path/to", query={"a": "1"}) + + + def test_clone_absolute_scheme() -> None: + req = make_mocked_request("GET", "https://example.com/path/to?a=1") +- assert req.scheme == "https" +- req2 = req.clone(scheme="http") +- assert req2.scheme == "http" +- assert req2.url.scheme == "http" ++ assert req.scheme == "http" ++ req2 = req.clone(scheme="https") ++ assert req2.scheme == "https" ++ assert req2.url.scheme == "https" ++ req3 = req2.clone(scheme="http") ++ assert req3.scheme == "http" ++ assert req3.url.scheme == "http" + + + def test_clone_absolute_host() -> None: +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p2.patch new file mode 100644 index 0000000000..1e04648c32 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111748_p2.patch @@ -0,0 +1,282 @@ +From 186065caf37d5999e91fd3d4ddfd03b43eb08801 Mon Sep 17 00:00:00 2001 +From: Andrew Svetlov +Date: Tue, 29 Sep 2026 11:25:53 +0200 +Subject: [PATCH] Reject absolute-form targets without an authority before + parsing (#13858) (#13860) + + + +Follow-up to #13855 for aio-libs/yarl#1937. With that change, yarl's +default WHATWG mode reads a host from `http:host/p`, `http:/host/p` and +`http:///host/p`, as browsers do. The Python and Cython request parsers +passed an absolute-form request target to `URL()` and rejected it only +if yarl found no host. They now first check that the target has `//` +followed by a non-empty authority, so `GET http:///protected` and `GET +http:/protected` stay rejected as RFC 9110 requires, whatever yarl +version is installed. + +The client tests used `http:///example.com` and `http:///path` as URLs +without a host. They now use `http://` where an invalid URL is needed, +and `http:///example.com` is dropped from the invalid URL test data. +With yarl#1937 it is `http://example.com/`, and as a redirect it leads +to `example.com`, as in browsers. + +No with the released yarl, where such targets were already rejected. +With yarl#1937, the server keeps rejecting absolute-form targets without +an authority instead of reading a host from them. + +No, it is one string check shared by both parsers. + +Follow-up to #13855; needed by aio-libs/yarl#1937. + +- [x] I think the code is well written +- [x] Unit tests for the changes exist +- [ ] Documentation reflects the changes: N/A, no documented behavior +changes +- [ ] If you provide code modification, please add yourself to +`CONTRIBUTORS.txt`: N/A, already listed +- [x] Add a new news fragment into the `CHANGES/` folder + +Drafted with Claude Code (Claude Opus 5.5); reviewed by @asvetlov. + +
+Agent run details (optional, for reviewers) + +Tests with the Cython extensions built (llhttp generated, `make +cythonize`, `build_ext --inplace`): `pytest tests -n auto`, 5246 passed, +22 skipped, 17 xfailed, both with yarl master (f761c37) and with the +yarl#1937 branch (8d0f111). Pure-Python run of `test_http_parser.py`, +`test_client_functional.py` and `test_client_session.py` with the +yarl#1937 branch: 884 passed. +Lint: black, isort, codespell passed. The local pre-commit flake8 hook +fails to load `flake8-requirements` (`pkg_resources` missing), so flake8 +(E, F, W) was run directly and passed. yesqa was skipped for the same +reason, because it wrongly strips `# noqa: I900`. +
+ +(cherry picked from commit ce960dd17e34f143762c14c32b9e14435ea7ce11) + + + +## What do these changes do? + + + +## Are there changes in behavior for the user? + + + +## Is it a substantial burden for the maintainers to support this? + + + +## Related issue number + + + + +## Checklist + +- [ ] I think the code is well written +- [ ] Unit tests for the changes exist +- [ ] Documentation reflects the changes +- [ ] If you provide code modification, please add yourself to +`CONTRIBUTORS.txt` + * The format is <Name> <Surname>. + * Please keep alphabetical order, the file is sorted by names. +- [ ] Add a new news fragment into the `CHANGES/` folder + * name it `..rst` (e.g. `588.bugfix.rst`) + * if you don't have an issue number, change it to the pull request + number after creating the PR + * `.bugfix`: A bug fix for something the maintainers deemed an + improper undesired behavior that got corrected to match + pre-agreed expectations. + * `.feature`: A new behavior, public APIs. That sort of stuff. + * `.deprecation`: A declaration of future API removals and breaking + changes in behavior. + * `.breaking`: When something public is removed in a breaking way. + Could be deprecated in an earlier release. + * `.doc`: Notable updates to the documentation structure or build + process. + * `.packaging`: Notes for downstreams about unobvious side effects + and tooling. Changes in the test invocation considerations and + runtime assumptions. + * `.contrib`: Stuff that affects the contributor experience. e.g. + Running tests, building the docs, setting up the development + environment. + * `.misc`: Changes that are hard to assign to any of the above + categories. + * Make sure to use full sentences with correct case and punctuation, + for example: + ```rst + Fixed issue with non-ascii contents in doctest text files + -- by :user:`contributor-gh-handle`. + ``` + + Use the past tense or the present tense a non-imperative mood, + referring to what's changed compared to the last released version + of this project. + +CVE: CVE-2026-111748 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/186065caf37d5999e91fd3d4ddfd03b43eb08801] + +Backport Changes: +- Omit the THREAT_MODEL.md update because that file is absent. +- Adapt the existing invalid-URL fixture list and redirect test. + Omit updates to newer redirect data and tests absent from 3.13.5. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13858.bugfix.rst | 7 +++++++ + aiohttp/_http_parser.pyx | 14 ++++++++------ + aiohttp/http_parser.py | 16 ++++++++++++++++ + tests/test_client_functional.py | 6 ++---- + tests/test_http_parser.py | 17 ++++++++++++++++- + 5 files changed, 49 insertions(+), 11 deletions(-) + create mode 100644 CHANGES/13858.bugfix.rst + +diff --git a/CHANGES/13858.bugfix.rst b/CHANGES/13858.bugfix.rst +new file mode 100644 +index 0000000..9bd2d11 +--- /dev/null ++++ b/CHANGES/13858.bugfix.rst +@@ -0,0 +1,7 @@ ++Rejected absolute-form request targets without ``//`` or with an empty ++host, such as ``http:/example.com/`` or ``http:///example.com/``, before ++parsing them with yarl, which reads a host from them in its WHATWG mode; ++RFC 9110 requires a host for ``http`` and ``https``. The invalid URL test ++data no longer uses ``http:///example.com``, which such a yarl version ++parses as ``http://example.com/``, as browsers do ++-- by :user:`asvetlov`. +diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx +index 4390253..694ccea 100644 +--- a/aiohttp/_http_parser.pyx ++++ b/aiohttp/_http_parser.pyx +@@ -30,7 +30,7 @@ from .http_exceptions import ( + PayloadEncodingError, + TransferEncodingError, + ) +-from .http_parser import DeflateBuffer as _DeflateBuffer ++from .http_parser import DeflateBuffer as _DeflateBuffer, _has_authority + from .http_writer import ( + HttpVersion as _HttpVersion, + HttpVersion10 as _HttpVersion10, +@@ -719,11 +719,13 @@ cdef class HttpRequestParser(HttpParser): + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 +- try: +- self._url = URL(self._path, encoded=True) +- host = self._url.raw_host +- except ValueError: +- host = None ++ host = None ++ if _has_authority(self._path): ++ try: ++ self._url = URL(self._path, encoded=True) ++ host = self._url.raw_host ++ except ValueError: ++ pass + # https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1-4 + if host is None: + raise InvalidURLError( +diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py +index 1c0650a..4687768 100644 +--- a/aiohttp/http_parser.py ++++ b/aiohttp/http_parser.py +@@ -89,6 +89,18 @@ VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) + DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) + HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") + ++ ++def _has_authority(target: str) -> bool: ++ """Tell if an absolute-form request target has "//" and a non-empty authority. ++ ++ RFC 9110 section 4.2 requires a host for http and https. yarl's default ++ WHATWG mode reads a host from "http:host/p" or "http:///host/p", so the ++ target is checked before it is parsed. ++ """ ++ rest = target.partition(":")[2] ++ return rest[:2] == "//" and rest[2:3] not in ("", "/", "?", "#") ++ ++ + # RFC 9110 singleton headers — duplicates are rejected in strict mode. + # In lax mode (response parser default), the check is skipped entirely + # since real-world servers (e.g. Google APIs, Werkzeug) commonly send +@@ -699,6 +711,10 @@ class HttpRequestParser(HttpParser[RawRequestMessage]): + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 ++ if not _has_authority(path): ++ raise InvalidURLError( ++ path.encode(errors="surrogateescape").decode("latin1") ++ ) + try: + url = URL(path, encoded=True) + host = url.raw_host +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 3415b83..eac6d81 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -3129,8 +3129,6 @@ INVALID_URL_WITH_ERROR_MESSAGE_YARL_NEW = ( + INVALID_URL_WITH_ERROR_MESSAGE_YARL_ORIGIN = ( + # # yarl.URL.origin raises ValueError + ("http:/", "http:///"), +- ("http:/example.com", "http:///example.com"), +- ("http:///example.com", "http:///example.com"), + ) + + NON_HTTP_URL_WITH_ERROR_MESSAGE = ( +@@ -5458,8 +5456,8 @@ async def test_invalid_redirect_origin_closes_payload( + async def redirect_handler(request: web.Request) -> web.Response: + # Read the payload to simulate server processing + await request.read() +- # Return a URL that will fail origin() check - using a relative URL without host +- return web.Response(status=307, headers={hdrs.LOCATION: "http:///path"}) ++ # Return a URL that will fail origin() check - using a URL without host ++ return web.Response(status=307, headers={hdrs.LOCATION: "http://"}) + + app = web.Application() + app.router.add_post("/redirect", redirect_handler) +diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py +index 610b77e..1397c1f 100644 +--- a/tests/test_http_parser.py ++++ b/tests/test_http_parser.py +@@ -913,8 +913,23 @@ def test_url_authority_form_only_connect(parser: HttpRequestParser) -> None: + b"https:////protected", + b"https://:80/protected", + b"https://user@/protected", ++ b"https://", ++ b"https://?q", ++ b"http:protected/x", ++ b"http:/protected/x", ++ b"http:\\\\protected/x", ++ ), ++ ids=( ++ "empty-host", ++ "empty-host-extra-slash", ++ "port-only", ++ "userinfo-only", ++ "no-authority", ++ "empty-host-query", ++ "no-slashes", ++ "one-slash", ++ "backslashes", + ), +- ids=("empty-host", "empty-host-extra-slash", "port-only", "userinfo-only"), + ) + def test_url_absolute_form_empty_host_rejected( + parser: HttpRequestParser, target: bytes +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index e2ac1eda3a..10817e6f34 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -27,6 +27,8 @@ SRC_URI += " \ file://CVE-2026-111750.patch \ file://CVE-2026-111749.patch \ file://CVE-2026-111065.patch \ + file://CVE-2026-111748_p1.patch \ + file://CVE-2026-111748_p2.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:27:01 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103228 Received: from rcdn-iport-9.cisco.com (rcdn-iport-9.cisco.com [173.37.86.80]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1308.1793950038054130967 for ; Thu, 05 Nov 2026 23:27:18 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=R9jWJc4R; spf=pass (domain: cisco.com, ip: 173.37.86.80, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=16986; q=dns/txt; s=iport01; t=1793950038; x=1795159638; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=567aiopc1a9k4iALwzxhOAmCip3i2clPQlo9Art7Wuw=; b=R9jWJc4R4gR8XqV8uBKgeeca/PHHyi7IR2ZoaKAmWzC/qsTk73po7Vaq uN0lBayItHxDJpRZi6FVBwFIimoC4yHUTeRda9ydvJn1LDmbOgZKEUKKT twlOoD3lxuu8kyxamv/Q1jFkPkzuLrWR13G4iDnU6/dDZsBK/7zub5pZk TSqPYqWwN+t/m25yB5SVcBC3BNLvzdBmQzPBFAB50EwStGoL1IJ4CHcKi h8t9Cc5ZFjIVyttL+eiC38uZnPwMQmYnlPrv5mrU5JddqCDe4Q98yGOpo n4S+h5nDBzIR6607rEFZP5N2luLjsW0U0DT54vt/ePnEoshKzcWV80FBH A==; X-CSE-ConnectionGUID: svxaopVbRiu5xSfmBS81bA== X-CSE-MsgGUID: 2S8GvHaFRFOw2BxCJENx3A== X-IPAS-Result: A0BJAgDmf+1q/47/Ja1aglmCV3VgQ0kDlkgDgx2bBIF+DwEBAQ9EDQQBAYQ/RgKOHQImNAkOAQIEAwIDAQEBAQEBAQEBAQEBCgEBBQEBAQIBBwWBDhOGTw2QEgECAQMnCwEZLRAcAgEBAi8kByMIGYMCAYJxAwMRRrtMgXkzgQGDKoFU2zMBCxQBgTiBc4NNiCNdGAFEhDkIHxsbgXKBFYE7gi6BBYFcAoFIhl4Egg0VgQyBWoEDgiKJcwKBUYZ5SIEeA1ksAVUTDQoLBwWBaAM1EioVbjIdgSM+F4EHGwYFgR2BHoESgQddgSsoXQEhgQiBdgKCUYIAAgFJQQ4HRVMJJUMSRyQiCBIJARMaLAqBGjU8CSY/FQxkCxoFYQgCDQx0ORAwHiQvBRwmPhsKLxgNSBEsNxUZBD5uB5I9gxIBLDQtASoBBHshgQcBBAYBBwENApMZkj+hEQoog3iMJJU6GjOFW6UWC5h9hVODLoI2glOCYQaGHYx9A02EaYFoPIFZcBU7gmcJShkPYY1Yg2uFZMZNJjICCQMvAQEHAgcOAwuBaJF+AQE IronPort-Data: A9a23:QP0wHas2JPTFIuBJoe95eI1oK+fnVAZfMUV32f8akzHdYApBsoF/q tZmKWuAMquJY2T3ftsgaIXn/U5Q6pGHz9ZrHgc4/nswFi5EgMeUXt7xwmUckM+xwmwvaGo9s q3yv/GZdJhcokf0/0nrb/656yYsjclkf5KkYMbcICd9WAR4fykojBNnioYRj5Vh6TSDK1vlV eja/YuFZzdJ5xYuajhKsvvb9Us21BjPkGpwUmIWNKgjUGD2zxH5PLpHTYmtIn3xRJVjH+LSb 47r0LGj82rFyAwmA9Wjn6yTWhVirmn6ZFXmZtJ+AsBOszAazsAA+v9T2Mk0NS+7vw60c+VZk 72hg3AfpTABZcUgkMxFO/VR/roX0aduoNcrKlDn2SCfItGvn3bEm51T4E8K0YIw4fhnCGZWy /YiKS1QfiuNgsiNkOuxY7w57igjBJGD0II3oHpsy3TdSP0hW52GG/iM7t5D1zB2jcdLdRrcT 5NGMnw0M1KaPkAJYwtNYH49tL/Aan3Xfz5VrFuUtKMf6GnIxws327/oWDbQUoHWGZwNxRbG+ woq+UyjOUE1KvyW5wCr1W32rP/kvw7dSNk7QejQGvlCxQf7KnYoIBoOWF22pPO0hkKzV5dTJ lIZ/gIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sZNZottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvrUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:2CnOL65IPj6Y5YTb+QPXwOrXdLJyesId70hD6qm+c3Nom6uj5q eTdZsgtCMc5Ax9ZJhko6HjBEDiewK5yXcK2+ks1N6ZNWGM0ldAbrsSiLcKqAePJ8SRzIJgPN 9bAstDIey1K0RmhsDn5wT9OdMhzN6btJ2Mv47lvhFQpcUAUdAZ0++/YTzra3FLeA== X-Talos-CUID: 9a23:bYePx2GG7JMsblLKqmJ86l4dHdgbeEfc0SrAGEG9I105Ebe8HAo= X-Talos-MUID: 9a23:z6DlKQVW97tuHMHq/GXX1B58FM5G2YqJFxongb4klu+UbgUlbg== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="544103865" Received: from rcdn-l-core-05.cisco.com ([173.37.255.142]) by rcdn-iport-9.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:17 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-05.cisco.com (Postfix) with ESMTPS id D9EA41800036A; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 3892CCBF234; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 6/7] python3-aiohttp: fix CVE-2026-111063 Date: Thu, 5 Nov 2026 23:27:01 -0800 Message-Id: <20261106072702.502813-7-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-05.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:20 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131969 From: Darsh Kelaiya Backport the Secure shared-cookie fix [1] to aiohttp 3.13.5, as identified by [3]. Apply the Secure check before looking up cached shared morsels so an earlier HTTPS request cannot bypass it. Include the upstream trusted-origin correction [2] so per-request cookies retain the session jar's treat_as_secure_origin policy. The default property keeps existing custom cookie jars compatible. Include the upstream cookie and client regression tests. [1] https://github.com/aio-libs/aiohttp/commit/999c356d61e966faffa97b06ab0d0455226d36c5 [2] https://github.com/aio-libs/aiohttp/commit/95081f8a353be2873643ab6dd74904ca413e45e0 [3] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-494j-54xm-rwp3 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111063_p1.patch | 137 ++++++++++ .../python3-aiohttp/CVE-2026-111063_p2.patch | 244 ++++++++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 2 + 3 files changed, 383 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p2.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p1.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p1.patch new file mode 100644 index 0000000000..f66cdf7d53 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p1.patch @@ -0,0 +1,137 @@ +From 999c356d61e966faffa97b06ab0d0455226d36c5 Mon Sep 17 00:00:00 2001 +From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> +Date: Sun, 27 Sep 2026 21:27:34 +0100 +Subject: [PATCH] [PR #13830/e11d2836 backport][3.14] Fix secure shared cookie + (#13832) + +**This is a backport of PR #13830 as merged into master +(e11d2836203a21bec59095498e578d37801027e7).** + +Co-authored-by: Sam Bull + +CVE: CVE-2026-111063 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/999c356d61e966faffa97b06ab0d0455226d36c5] + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13830.bugfix.rst | 1 + + aiohttp/cookiejar.py | 3 +++ + docs/client_reference.rst | 3 ++- + docs/spelling_wordlist.txt | 1 + + tests/test_client_functional.py | 9 +++++++-- + tests/test_cookiejar.py | 28 ++++++++++++++++++++++++++++ + 6 files changed, 42 insertions(+), 3 deletions(-) + create mode 100644 CHANGES/13830.bugfix.rst + +diff --git a/CHANGES/13830.bugfix.rst b/CHANGES/13830.bugfix.rst +new file mode 100644 +index 0000000..2a39769 +--- /dev/null ++++ b/CHANGES/13830.bugfix.rst +@@ -0,0 +1 @@ ++Fixed ``CookieJar.filter_cookies()`` sending shared cookies (cookies without a ``Domain`` attribute) marked ``Secure`` over unencrypted connections -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index 6712b90..78630b5 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -471,6 +471,9 @@ class CookieJar(AbstractCookieJar): + # Send shared cookie + key = ("", "") + for c in self._cookies[key].values(): ++ if is_not_secure and c["secure"]: ++ continue ++ + # Check cache first + if c.key in self._morsel_cache[key]: + filtered[c.key] = self._morsel_cache[key][c.key] +diff --git a/docs/client_reference.rst b/docs/client_reference.rst +index 63ee375..24f9088 100644 +--- a/docs/client_reference.rst ++++ b/docs/client_reference.rst +@@ -2464,7 +2464,8 @@ Utilities + :param ~yarl.URL response_url: URL of response, ``None`` for *shared + cookies*. Regular cookies are coupled with server's URL and + are sent only to this server, shared ones are sent in every +- client request. ++ client request (except that shared cookies marked ``Secure`` ++ are only sent over encrypted connections). + + .. method:: filter_cookies(request_url) + +diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt +index e429f8c..e04017c 100644 +--- a/docs/spelling_wordlist.txt ++++ b/docs/spelling_wordlist.txt +@@ -350,6 +350,7 @@ un + unawaited + unclosed + undercounting ++unencrypted + unescaped + unhandled + unicode +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index 3415b83..a92d008 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -2843,12 +2843,17 @@ async def test_morsel_with_attributes(aiohttp_client) -> None: + c = http.cookies.Morsel() + c.set("test3", "456", "456") + c["httponly"] = True +- c["secure"] = True + c["max-age"] = 1000 + ++ # A Secure shared cookie must be withheld entirely: the test server ++ # is plain http, so it must not reach the handler at all. ++ c2: http.cookies.Morsel[str] = http.cookies.Morsel() ++ c2.set("test4", "789", "789") ++ c2["secure"] = True ++ + app = web.Application() + app.router.add_get("/", handler) +- client = await aiohttp_client(app, cookies={"test2": c}) ++ client = await aiohttp_client(app, cookies={"test2": c, "test4": c2}) + + async with client.get("/") as resp: + assert 200 == resp.status +diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py +index 9a061a3..d4bfdcc 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -1155,6 +1155,34 @@ async def test_treat_as_secure_origin() -> None: + assert len(filtered_cookies) == 1 + + ++async def test_treat_as_secure_origin_sends_secure_shared_cookie() -> None: ++ endpoint = URL("http://127.0.0.1/") ++ ++ jar = CookieJar(unsafe=True, treat_as_secure_origin=[endpoint]) ++ jar.update_cookies(SimpleCookie("shared-secure-cookie=first; Secure;")) ++ ++ filtered_cookies = jar.filter_cookies(request_url=endpoint) ++ assert "shared-secure-cookie" in filtered_cookies ++ ++ ++async def test_filter_cookies_with_secure_shared_cookie() -> None: ++ """Secure shared cookies (no Domain attribute) must not be sent over cleartext.""" ++ jar = CookieJar() ++ jar.update_cookies( ++ SimpleCookie("shared-secure-cookie=first; Secure; shared-cookie=second;") ++ ) ++ ++ filtered = jar.filter_cookies(URL("http://example.com/")) ++ assert set(filtered.keys()) == {"shared-cookie"} ++ ++ filtered = jar.filter_cookies(URL("https://example.com/")) ++ assert set(filtered.keys()) == {"shared-cookie", "shared-secure-cookie"} ++ ++ # The morsel cached by the https request must not bypass the Secure check. ++ filtered = jar.filter_cookies(URL("http://example.com/")) ++ assert set(filtered.keys()) == {"shared-cookie"} ++ ++ + async def test_filter_cookies_does_not_leak_memory() -> None: + """Test that filter_cookies doesn't create empty cookie entries. + +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p2.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p2.patch new file mode 100644 index 0000000000..b71f531a36 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111063_p2.patch @@ -0,0 +1,244 @@ +From 95081f8a353be2873643ab6dd74904ca413e45e0 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Mon, 28 Sep 2026 00:38:13 +0100 +Subject: [PATCH] Fix treat_as_secure_origin on request cookies (#13833) + (#13836) + +(cherry picked from commit 6bc84990f2a6429667f741d97f5da22884b8a8d3) + +CVE: CVE-2026-111063 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/95081f8a353be2873643ab6dd74904ca413e45e0] + +Backport Changes: +- Apply to aiohttp 3.13.5 and retain its Union type spelling. +- Pass trusted origins through the existing quote_cookie constructor + context without adding the newer unsafe inspection API. +- Add the property documentation beside the existing attributes; + the newer public cookie-inspection sections are absent. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13833.bugfix.rst | 1 + + aiohttp/abc.py | 5 ++++ + aiohttp/client.py | 5 +++- + aiohttp/cookiejar.py | 26 +++++++++++------ + docs/client_reference.rst | 13 +++++++-- + tests/test_client_functional.py | 49 +++++++++++++++++++++++++++++++++ + tests/test_cookiejar.py | 3 +- + 7 files changed, 89 insertions(+), 13 deletions(-) + create mode 100644 CHANGES/13833.bugfix.rst + +diff --git a/CHANGES/13833.bugfix.rst b/CHANGES/13833.bugfix.rst +new file mode 100644 +index 0000000..2d0f96c +--- /dev/null ++++ b/CHANGES/13833.bugfix.rst +@@ -0,0 +1 @@ ++Fixed per-request cookies (the ``cookies`` argument of a request method) marked ``Secure`` not being sent to origins listed in ``CookieJar``'s ``treat_as_secure_origin`` -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/abc.py b/aiohttp/abc.py +index faf0957..82afc6c 100644 +--- a/aiohttp/abc.py ++++ b/aiohttp/abc.py +@@ -182,6 +182,11 @@ class AbstractCookieJar(Sized, IterableBase): + def quote_cookie(self) -> bool: + """Return True if cookies should be quoted.""" + ++ @property ++ def treat_as_secure_origin(self) -> frozenset[URL]: ++ """Return origins considered secure even over cleartext connections.""" ++ return frozenset() ++ + @abstractmethod + def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None: + """Clear all cookies if no predicate is passed.""" +diff --git a/aiohttp/client.py b/aiohttp/client.py +index f404b55..1eae60a 100644 +--- a/aiohttp/client.py ++++ b/aiohttp/client.py +@@ -692,7 +692,10 @@ class ClientSession: + + if cookies is not None: + tmp_cookie_jar = CookieJar( +- quote_cookie=self._cookie_jar.quote_cookie ++ quote_cookie=self._cookie_jar.quote_cookie, ++ treat_as_secure_origin=frozenset( ++ self._cookie_jar.treat_as_secure_origin ++ ), + ) + tmp_cookie_jar.update_cookies(cookies) + req_cookies = tmp_cookie_jar.filter_cookies(url) +diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py +index 78630b5..3b2c490 100644 +--- a/aiohttp/cookiejar.py ++++ b/aiohttp/cookiejar.py +@@ -129,7 +129,7 @@ class CookieJar(AbstractCookieJar): + *, + unsafe: bool = False, + quote_cookie: bool = True, +- treat_as_secure_origin: Union[StrOrURL, List[StrOrURL], None] = None, ++ treat_as_secure_origin: Union[StrOrURL, Iterable[StrOrURL], None] = None, + loop: Optional[asyncio.AbstractEventLoop] = None, + ) -> None: + super().__init__(loop=loop) +@@ -144,17 +144,20 @@ class CookieJar(AbstractCookieJar): + self._unsafe = unsafe + self._quote_cookie = quote_cookie + if treat_as_secure_origin is None: +- treat_as_secure_origin = [] ++ self._treat_as_secure_origin: frozenset[URL] = frozenset() + elif isinstance(treat_as_secure_origin, URL): +- treat_as_secure_origin = [treat_as_secure_origin.origin()] ++ self._treat_as_secure_origin = frozenset({treat_as_secure_origin.origin()}) + elif isinstance(treat_as_secure_origin, str): +- treat_as_secure_origin = [URL(treat_as_secure_origin).origin()] ++ self._treat_as_secure_origin = frozenset( ++ {URL(treat_as_secure_origin).origin()} ++ ) + else: +- treat_as_secure_origin = [ +- URL(url).origin() if isinstance(url, str) else url.origin() +- for url in treat_as_secure_origin +- ] +- self._treat_as_secure_origin = treat_as_secure_origin ++ self._treat_as_secure_origin = frozenset( ++ { ++ URL(url).origin() if isinstance(url, str) else url.origin() ++ for url in treat_as_secure_origin ++ } ++ ) + self._expire_heap: List[Tuple[float, Tuple[str, str, str]]] = [] + self._expirations: Dict[Tuple[str, str, str], float] = {} + +@@ -162,6 +165,11 @@ class CookieJar(AbstractCookieJar): + def quote_cookie(self) -> bool: + return self._quote_cookie + ++ @property ++ def treat_as_secure_origin(self) -> frozenset[URL]: ++ """Return origins considered secure even over cleartext connections.""" ++ return self._treat_as_secure_origin ++ + def save(self, file_path: PathLike) -> None: + """Save cookies to a file using JSON format. + +diff --git a/docs/client_reference.rst b/docs/client_reference.rst +index 24f9088..353047d 100644 +--- a/docs/client_reference.rst ++++ b/docs/client_reference.rst +@@ -2441,7 +2441,7 @@ Utilities + .. versionadded:: 3.7 + + :param treat_as_secure_origin: (optional) Mark origins as secure +- for cookies marked as Secured. Possible types are ++ for cookies marked as Secured. + + Possible types are: + +@@ -2465,7 +2465,8 @@ Utilities + cookies*. Regular cookies are coupled with server's URL and + are sent only to this server, shared ones are sent in every + client request (except that shared cookies marked ``Secure`` +- are only sent over encrypted connections). ++ are only sent over encrypted connections or to origins listed ++ in *treat_as_secure_origin*). + + .. method:: filter_cookies(request_url) + +@@ -2521,6 +2522,14 @@ Utilities + + .. versionadded:: 4.0 + ++ .. attribute:: treat_as_secure_origin ++ ++ A :class:`frozenset` of :class:`~yarl.URL` origins that are ++ treated as secure even when the connection is not encrypted, as ++ configured by the *treat_as_secure_origin* parameter. ++ ++ .. versionadded:: 3.14.4 ++ + + .. class:: DummyCookieJar(*, loop=None) + +diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py +index a92d008..e0bb02e 100644 +--- a/tests/test_client_functional.py ++++ b/tests/test_client_functional.py +@@ -2859,6 +2859,55 @@ async def test_morsel_with_attributes(aiohttp_client) -> None: + assert 200 == resp.status + + ++async def test_request_secure_cookie_treat_as_secure_origin( ++ aiohttp_server: AiohttpServer, aiohttp_client: AiohttpClient ++) -> None: ++ """Per-request Secure cookies must honor the session jar's trusted origins.""" ++ ++ async def handler(request: web.Request) -> web.Response: ++ assert request.cookies.get("auth") == "token" ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/", handler) ++ server = await aiohttp_server(app) ++ ++ jar = aiohttp.CookieJar(unsafe=True, treat_as_secure_origin=[server.make_url("/")]) ++ client = await aiohttp_client(server, cookie_jar=jar) # type: ignore[var-annotated] ++ ++ c: http.cookies.Morsel[str] = http.cookies.Morsel() ++ c.set("auth", "token", "token") ++ c["secure"] = True ++ ++ async with client.get("/", cookies={"auth": c}) as resp: ++ assert resp.status == 200 ++ ++ ++async def test_request_secure_cookie_not_sent_over_http( ++ aiohttp_client: AiohttpClient, ++) -> None: ++ """A per-request Secure cookie is only sent to the jar's trusted origins.""" ++ ++ async def handler(request: web.Request) -> web.Response: ++ assert request.cookies.keys() == {"plain"} ++ return web.Response() ++ ++ app = web.Application() ++ app.router.add_get("/", handler) ++ # The trusted origin never matches the test server, so the Secure ++ # cookie must still be withheld from this plain-http request. ++ jar = aiohttp.CookieJar(treat_as_secure_origin=[URL("http://example.com")]) ++ client = await aiohttp_client(app, cookie_jar=jar) ++ ++ c: http.cookies.Morsel[str] = http.cookies.Morsel() ++ c.set("auth", "token", "token") ++ c["secure"] = True ++ ++ cookies: dict[str, str | http.cookies.Morsel[str]] = {"auth": c, "plain": "ok"} ++ async with client.get("/", cookies=cookies) as resp: ++ assert resp.status == 200 ++ ++ + async def test_set_cookies( + aiohttp_client: AiohttpClient, caplog: pytest.LogCaptureFixture + ) -> None: +diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py +index d4bfdcc..4c7107c 100644 +--- a/tests/test_cookiejar.py ++++ b/tests/test_cookiejar.py +@@ -830,6 +830,7 @@ async def test_dummy_cookie_jar() -> None: + cookie = SimpleCookie("foo=bar; Domain=example.com;") + dummy_jar = DummyCookieJar() + assert dummy_jar.quote_cookie is True ++ assert dummy_jar.treat_as_secure_origin == frozenset() + assert len(dummy_jar) == 0 + dummy_jar.update_cookies(cookie) + assert len(dummy_jar) == 0 +@@ -1134,7 +1135,7 @@ async def test_pickle_format(cookies_to_send) -> None: + ) + async def test_treat_as_secure_origin_init(url) -> None: + jar = CookieJar(unsafe=True, treat_as_secure_origin=url) +- assert jar._treat_as_secure_origin == [URL("http://127.0.0.1")] ++ assert jar.treat_as_secure_origin == frozenset({URL("http://127.0.0.1")}) + + + async def test_treat_as_secure_origin() -> None: +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index 10817e6f34..c6167ef607 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -29,6 +29,8 @@ SRC_URI += " \ file://CVE-2026-111065.patch \ file://CVE-2026-111748_p1.patch \ file://CVE-2026-111748_p2.patch \ + file://CVE-2026-111063_p1.patch \ + file://CVE-2026-111063_p2.patch \ " CVE_PRODUCT = "aiohttp:aiohttp" From patchwork Fri Nov 6 07:27:02 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 103227 Received: from rcdn-iport-6.cisco.com (rcdn-iport-6.cisco.com [173.37.86.77]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.1307.1793950037989779656 for ; Thu, 05 Nov 2026 23:27:18 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=Wn2khoz/; spf=pass (domain: cisco.com, ip: 173.37.86.77, mailfrom: dkelaiya@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=12705; q=dns/txt; s=iport01; t=1793950038; x=1795159638; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=jAB92yRzpuxIvULUQvrGgbe2owRQvhM0QV7Dp0y78Ak=; b=Wn2khoz/Gf/heUhzNL31mCxGvk21LPlxoHz9QZSB8nt1Za2/kHKdVsNU pCsboZjFXnYNWFPwBJcdyzJBaGLC7HEpiLXMqUVB5r91VjfXnwahwcosf SN+8J/pirhoW98DSgv3stXIvmANmrXoPv63uF9YLu3riBz11roXWJFaG0 tAqtTc4UBk8Vs7W2OUdcM0gVCnWe1Sh1npfhuJvmdvmTA9H44JuS2T6zE n/Ugwa69QzaXyFNzKc5pj9XqWklyNOhvGXNbpFRS7JP2egUDn1Kd4RvUd +iHwTUx9ibPWRR0iPsfCUCdT8yd7zR7ytvYX9BKx03inH+UcZCqNpdeC9 w==; X-CSE-ConnectionGUID: ISxbqs2+RWKUsBfH3/sbHw== X-CSE-MsgGUID: +/EnmFaYTq6oLWiwH87HHg== X-IPAS-Result: A0BJAgDmf+1q/43/Ja1aglmCGD91YENJA5ZIA5FMjFWBfg8BAQEPRA0EAQGEP0YCjh0CJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBAgEDJwsBGAEtEBwDAQIvKyMIGYIqWAGCdAMRvBKBeTOBAYMpAYFU2zMBCxQBgTiFQIgjXRgBhH0nGxuBcoEVgTuCLoEFgVwCgUiGXgSCIoEMgVoBhjwDhVOBBQKBUYZ5SIEeA1ksAVUTDQoLBwWBaAM1EioVbjIdgSM+F4EHGwYFgR2BHoESgQddgSsoXQEhgQiBdgKCUYIAAgFJQQ4HRVMJJUMSRyQiCBIJARMaLAqBGjU8CSY/FQxkCxoFYQgCDQx0ORAwHiQvBRwmPhsKLxgNSBEsNxUZBD5uB5I9gxIOCEQDBBoGDQEqAQSBAQxGEUQGFjKSZwECkj6hEQoog3iMJIcnjhMaM4QElBmSVAuYfY4KlWMCBBgDTYRpgWg8gVlwFYMiCUoZD1iNUw4Lg2DMMSYyAgkDLwEBBwIHDgMLgWiRfgEB IronPort-Data: A9a23:5XXMRKme32uZP292J3u0J0ro5gzRJ0RdPkR7XQ2eYbSJt1+Wr1Gzt xIYCjyGPvqPajPyeNAnOdm08ExQupeBxtYyTFNl+3gzE1tH+JHPbTi7wugcHM8zwunrFh8PA xA2M4GYRCwMZiaC4Errav6+/SEUOZigHtLUEPTDNj16WThqQSIgjQMLs+Mii+aEu/Dha++2k Y20+ZK31GONgWYubDtNs//b8XuDgdyr0N8mlg1mDRx0lAe2e0k9VPo3Oay3Jn3kdYhYdsbSb /rD1ryw4lTC9B4rDN6/+p6jGqHdauePVeQmoiM+t5mK2nCulARrukoIHKZ0hXNsttm8t4sZJ OOhGnCHYVxB0qXkwIzxWvTDes10FfUuFLTveRBTvSEPpqHLWyOE/hlgMK05Fawj6uxuGjsNz 8M7FSsnYDDYwPm84K3uH4GAhux7RCXqFJkUtnclyXTSCuwrBMifBa7L/tRfmjw3g6iiH96HO JFfMmUpNkmdJUQTYz/7C7pm9AusrnX8fjlRqUOcjaE2+GPUigd21dABNfKIJoDbG5sFxxfwS mTuvCPUBBQ0Pe2j0GSC1k2tmc+fwgPbV9dHfFG/3rsw6LGJ/UQUEBAQWF6xrPW1h0L7V99BJ kg8/is1sbN081SmSNT4VRC0rHOI+BkGVLJt//YS8gqBzO/Qpg2eHGVBFmIHY909v8hwTjsvv rOUo+7U6fVUmOX9YRqgGn289Fte5QB9wbc+WBI5 IronPort-HdrOrdr: A9a23:rKKf1KOTkd7QscBcTuOjsMiBIKoaSvp037Dk7S9MoHtuA6ulfq +V/cjzuSWYtN9VYgBDpTniAtjlfZqjz/5ICOAqVN/INjUO+lHYSb2KhrGN/9SPIUHDH5ZmpM RdWpk7LsHsBl5nisu/ygy5H9E8hOSjysmT9IHjJ7MHd3ATV0mmhD0JczqmLg== X-Talos-CUID: 9a23:N4jPR2uiiBQjDxaPu5SSQ9506IsESW3X1WyKYHWHKltyYrO1EhiL8oN7xp8= X-Talos-MUID: 9a23:VcCHdg4TB729J03tx2Wh7f87xowrpImoDW4EoKkf5c6gCi1vG2fCrBS4F9o= X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,197,1787011200"; d="scan'208";a="544966819" Received: from rcdn-l-core-04.cisco.com ([173.37.255.141]) by rcdn-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 06 Nov 2026 07:27:17 +0000 Received: from sjc-ads-5675.cisco.com (sjc-ads-5675.cisco.com [10.28.88.189]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by rcdn-l-core-04.cisco.com (Postfix) with ESMTPS id DB46518000191; Fri, 6 Nov 2026 07:27:16 +0000 (GMT) Received: by sjc-ads-5675.cisco.com (Postfix, from userid 1887444) id 3CBCBCBEF8A; Thu, 5 Nov 2026 23:27:16 -0800 (PST) From: "Darsh Kelaiya -X (dkelaiya - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-devel@lists.openembedded.org Cc: Darsh Kelaiya Subject: [oe][meta-python][wrynose][PATCH 7/7] python3-aiohttp: fix CVE-2026-111064 Date: Thu, 5 Nov 2026 23:27:02 -0800 Message-Id: <20261106072702.502813-8-dkelaiya@cisco.com> X-Mailer: git-send-email 2.35.6 In-Reply-To: <20261106072702.502813-1-dkelaiya@cisco.com> References: <20261106072702.502813-1-dkelaiya@cisco.com> MIME-Version: 1.0 X-Outbound-Client-TLS: ANONYMOUS;sjc-ads-5675.cisco.com [10.28.88.189];TLSv1.3;TLS_AES_256_GCM_SHA384;256 X-Outbound-SMTP-Client: 10.28.88.189, sjc-ads-5675.cisco.com X-Outbound-Node: rcdn-l-core-04.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Fri, 06 Nov 2026 07:27:19 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131968 From: Darsh Kelaiya Backport the initial idle-connection timeout fix [1] to aiohttp 3.13.5, as identified by [2]. Start the keepalive deadline when the connection is established so peers that send no request or trickle incomplete headers cannot keep it open indefinitely. Retain the existing timeout default and the upstream regressions for idle peers, trickled headers, disabled deadlines, slow handlers and WebSocket keepalive cleanup. [1] https://github.com/aio-libs/aiohttp/commit/be846822b4b3a5a54b6163e794b42dc06fba54a3 [2] https://github.com/aio-libs/aiohttp/security/advisories/GHSA-5wr8-gcqf-gqf3 Signed-off-by: Darsh Kelaiya --- .../python3-aiohttp/CVE-2026-111064.patch | 285 ++++++++++++++++++ .../python/python3-aiohttp_3.13.5.bb | 1 + 2 files changed, 286 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111064.patch diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111064.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111064.patch new file mode 100644 index 0000000000..9962a22ecb --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2026-111064.patch @@ -0,0 +1,285 @@ +From be846822b4b3a5a54b6163e794b42dc06fba54a3 Mon Sep 17 00:00:00 2001 +From: Sam Bull +Date: Tue, 29 Sep 2026 00:08:22 +0100 +Subject: [PATCH] Close idle connections (#13681) (#13838) + +(cherry picked from commit e07684ef9056183bbefba1ae3f717a23f243631e) + +CVE: CVE-2026-111064 +Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/be846822b4b3a5a54b6163e794b42dc06fba54a3] + +Backport Changes: +- Retain aiohttp 3.13.5's documented keepalive_timeout=3630 default + instead of changing it to the newer branch's 75-second default. +- Import Any and AiohttpServer from existing modules for the + upstream server and WebSocket regression tests. + +Signed-off-by: Darsh Kelaiya +--- + CHANGES/13681.bugfix.rst | 1 + + aiohttp/web_protocol.py | 12 ++- + docs/web_reference.rst | 9 ++ + tests/test_web_server.py | 118 ++++++++++++++++++++++++- + tests/test_web_websocket_functional.py | 34 ++++++- + 5 files changed, 167 insertions(+), 7 deletions(-) + create mode 100644 CHANGES/13681.bugfix.rst + +diff --git a/CHANGES/13681.bugfix.rst b/CHANGES/13681.bugfix.rst +new file mode 100644 +index 0000000..e39b73a +--- /dev/null ++++ b/CHANGES/13681.bugfix.rst +@@ -0,0 +1 @@ ++Fixed idle connections not being closed if no request was received -- by :user:`Dreamsorcerer`. +diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py +index d9843f9..c04aa10 100644 +--- a/aiohttp/web_protocol.py ++++ b/aiohttp/web_protocol.py +@@ -117,8 +117,7 @@ class RequestHandler(BaseProtocol): + status line, bad headers or incomplete payload. If any error occurs, + connection gets closed. + +- keepalive_timeout -- number of seconds before closing +- keep-alive connection ++ keepalive_timeout -- number of seconds before closing an idle connection. + + tcp_keepalive -- TCP keep-alive is on, default is on + +@@ -215,7 +214,7 @@ class RequestHandler(BaseProtocol): + + # _request_count is the number of requests processed with the same connection. + self._request_count = 0 +- self._keepalive = False ++ self._keepalive = True + self._current_request: Optional[BaseRequest] = None + self._manager: Optional[Server] = manager + self._request_handler: Optional[_RequestHandler] = manager.request_handler +@@ -367,6 +366,13 @@ class RequestHandler(BaseProtocol): + self._manager.connection_made(self, real_transport) + + loop = self._loop ++ # Need to enable keepalive timeout at start of connection, as there's no other ++ # protection against a dead connection that doesn't send a request at all. ++ if self._keepalive_timeout > 0: ++ close_time = loop.time() + self._keepalive_timeout ++ self._next_keepalive_close_time = close_time ++ self._keepalive_handle = loop.call_at(close_time, self._process_keepalive) ++ + if sys.version_info >= (3, 12): + task = asyncio.Task(self.start(), loop=loop, eager_start=True) + else: +diff --git a/docs/web_reference.rst b/docs/web_reference.rst +index 83a9c7f..ddb43e8 100644 +--- a/docs/web_reference.rst ++++ b/docs/web_reference.rst +@@ -3135,6 +3135,10 @@ Utilities + :param float keepalive_timeout: a delay before a TCP connection is + closed after a HTTP request. The delay + allows for reuse of a TCP connection. ++ The same delay bounds how long a newly ++ accepted connection may take to deliver ++ its first complete request; connections ++ that stay idle or incomplete are closed. + + When deployed behind a reverse proxy + it's important for this value to be +@@ -3144,6 +3148,11 @@ Utilities + + .. versionadded:: 3.8 + ++ .. versionchanged:: 3.14.4 ++ ++ The timeout is now also applied while waiting for the first ++ request, closing connections that never send a complete request. ++ + :param ssl_context: :class:`ssl.SSLContext` for HTTPS server, + ``None`` for HTTP connection. + +diff --git a/tests/test_web_server.py b/tests/test_web_server.py +index 09b7d0b..7595aec 100644 +--- a/tests/test_web_server.py ++++ b/tests/test_web_server.py +@@ -1,7 +1,7 @@ + import asyncio + import socket + from contextlib import suppress +-from typing import NoReturn ++from typing import Any, NoReturn + from unittest import mock + + import pytest +@@ -417,3 +417,119 @@ async def test_no_handler_cancellation(unused_port_socket: socket.socket) -> Non + assert done_event.is_set() + finally: + await asyncio.gather(runner.shutdown(), site.stop()) ++ ++ ++async def test_idle_connection_closed_before_first_request( ++ aiohttp_raw_server: AiohttpRawServer, ++) -> None: ++ """A connection that never sends a request must not be held open forever.""" ++ ++ async def handler(request: web.BaseRequest) -> web.Response: ++ assert False ++ ++ server = await aiohttp_raw_server(handler, keepalive_timeout=0.2) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ # The server must close the connection once keepalive_timeout ++ # expires without a complete request having arrived. ++ assert await asyncio.wait_for(reader.read(), timeout=5) == b"" ++ finally: ++ writer.close() ++ await writer.wait_closed() ++ ++ ++async def test_trickled_headers_closed_at_first_request_deadline( ++ aiohttp_raw_server: AiohttpRawServer, monkeypatch: pytest.MonkeyPatch ++) -> None: ++ """Partial header bytes must not extend the first-request deadline.""" ++ ++ async def handler(request: web.BaseRequest) -> web.Response: ++ assert False ++ ++ server = await aiohttp_raw_server(handler, keepalive_timeout=0.4) ++ runner = server.runner ++ assert runner is not None ++ server_impl = runner.server ++ assert server_impl is not None ++ ++ disconnected = asyncio.Event() ++ original_connection_lost = server_impl.connection_lost ++ ++ def connection_lost(protocol: Any, exc: BaseException | None = None) -> None: ++ original_connection_lost(protocol, exc) ++ disconnected.set() ++ ++ monkeypatch.setattr(server_impl, "connection_lost", connection_lost) ++ ++ _, writer = await asyncio.open_connection(server.host, server.port) ++ ++ async def trickle_until_closed() -> None: ++ writer.write(b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Slow: ") ++ # asyncio's drain() raises a ConnectionError once the connection is ++ # lost; uvloop's write() raises RuntimeError on the closed transport. ++ with pytest.raises((ConnectionError, RuntimeError)): ++ while True: ++ await writer.drain() ++ await asyncio.sleep(0.05) ++ writer.write(b"a") ++ ++ try: ++ # The deadline must fire despite the steady trickle of bytes. ++ await asyncio.wait_for(trickle_until_closed(), timeout=5) ++ await asyncio.wait_for(disconnected.wait(), timeout=5) ++ assert not server_impl.connections ++ assert server_impl.requests_count == 0 ++ finally: ++ writer.close() ++ with suppress(ConnectionError, RuntimeError): ++ await writer.wait_closed() ++ ++ ++async def test_keepalive_timeout_zero_no_first_request_deadline( ++ aiohttp_raw_server: AiohttpRawServer, ++) -> None: ++ """keepalive_timeout=0 must not impose a deadline on the first request.""" ++ ++ async def handler(request: web.BaseRequest) -> web.Response: ++ return web.Response(text="ok") ++ ++ server = await aiohttp_raw_server(handler, keepalive_timeout=0) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ # A zero timeout must not arm a deadline that fires at accept time: ++ # the connection has to survive an idle pause and serve the request. ++ await asyncio.sleep(0.1) ++ writer.write(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") ++ await writer.drain() ++ head = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=5) ++ assert head.startswith(b"HTTP/1.1 200 ") ++ # Between requests a zero timeout still closes the idle connection ++ # right after the response, so the body is followed by EOF. ++ assert await asyncio.wait_for(reader.read(), timeout=5) == b"ok" ++ finally: ++ writer.close() ++ await writer.wait_closed() ++ ++ ++async def test_handler_slower_than_first_request_deadline( ++ aiohttp_raw_server: AiohttpRawServer, ++) -> None: ++ """A parsed request being handled is not subject to the idle deadline.""" ++ ++ async def handler(request: web.BaseRequest) -> web.Response: ++ await asyncio.sleep(0.4) # Longer than keepalive_timeout. ++ return web.Response(text="ok") ++ ++ server = await aiohttp_raw_server(handler, keepalive_timeout=0.2) ++ ++ reader, writer = await asyncio.open_connection(server.host, server.port) ++ try: ++ writer.write(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") ++ await writer.drain() ++ head = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=5) ++ assert head.startswith(b"HTTP/1.1 200 ") ++ finally: ++ writer.close() ++ await writer.wait_closed() +diff --git a/tests/test_web_websocket_functional.py b/tests/test_web_websocket_functional.py +index f7f5c31..871987e 100644 +--- a/tests/test_web_websocket_functional.py ++++ b/tests/test_web_websocket_functional.py +@@ -12,7 +12,7 @@ import pytest + import aiohttp + from aiohttp import web + from aiohttp.http import WSCloseCode, WSMsgType +-from aiohttp.pytest_plugin import AiohttpClient ++from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer + + + async def test_websocket_can_prepare(loop, aiohttp_client) -> None: +@@ -953,9 +953,37 @@ async def test_websocket_disable_keepalive(loop, aiohttp_client) -> None: + assert data == "OK" + + +-async def test_receive_str_nonstring( +- loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient ++async def test_websocket_disable_keepalive_no_armed_handle( ++ aiohttp_server: AiohttpServer, aiohttp_client: AiohttpClient + ) -> None: ++ """keep_alive(False) when no keepalive timer was armed (keepalive_timeout=0).""" ++ ++ async def handler(request: web.Request) -> web.StreamResponse: ++ ws = web.WebSocketResponse() ++ assert ws.can_prepare(request) ++ # A zero timeout arms no deadline at connection_made(). ++ assert request.protocol._keepalive_handle is None ++ await ws.prepare(request) ++ assert not request.protocol._keepalive ++ assert request.protocol._keepalive_handle is None ++ ++ await ws.send_str("OK") ++ await ws.close() ++ return ws ++ ++ app = web.Application() ++ app.router.add_route("GET", "/", handler) ++ server = await aiohttp_server(app, keepalive_timeout=0) ++ client = await aiohttp_client(server) # type: ignore[var-annotated] ++ ++ ws = await client.ws_connect("/") ++ data = await ws.receive_str() ++ assert data == "OK" ++ ++ await ws.receive() # Handle close ++ ++ ++async def test_receive_str_nonstring(aiohttp_client: AiohttpClient) -> None: + async def handler(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + assert ws.can_prepare(request) +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb index c6167ef607..98fd4bdba3 100644 --- a/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb +++ b/meta-python/recipes-devtools/python/python3-aiohttp_3.13.5.bb @@ -31,6 +31,7 @@ SRC_URI += " \ file://CVE-2026-111748_p2.patch \ file://CVE-2026-111063_p1.patch \ file://CVE-2026-111063_p2.patch \ + file://CVE-2026-111064.patch \ " CVE_PRODUCT = "aiohttp:aiohttp"