mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-14 03:57:15 +00:00
python3-aiohttp: fix CVE-2026-111063
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 <dkelaiya@cisco.com> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
1 parent
9244a2e741
commit
70b574354e
3 files changed
+383
No files matched your search
@@ -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 <git@sambull.org>
|
||||
|
||||
CVE: CVE-2026-111063
|
||||
Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/999c356d61e966faffa97b06ab0d0455226d36c5]
|
||||
|
||||
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,244 @@
|
||||
From 95081f8a353be2873643ab6dd74904ca413e45e0 Mon Sep 17 00:00:00 2001
|
||||
From: Sam Bull <git@sambull.org>
|
||||
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 <dkelaiya@cisco.com>
|
||||
---
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user