File CVE-2022-23607-bind-cookies-to-domain.patch of Package python-treq.17624

From da59847f06a875cd3e6784dc0f326d13e1a389ac Mon Sep 17 00:00:00 2001
From: Glyph <glyph@twistedmatrix.com>
Date: Fri, 28 Jan 2022 15:44:21 -0800
Subject: [PATCH 1/4] scope cookies by default

---
 changelog.d/339.bugfix.rst             |    1 
 src/treq/client.py                     |   63 ++++++++++++++++++++++++++++++---
 src/treq/test/test_testing.py          |   41 ++++++++++++++++++++-
 src/treq/test/test_treq_integration.py |    1 
 4 files changed, 100 insertions(+), 6 deletions(-)

--- /dev/null
+++ b/changelog.d/339.bugfix.rst
@@ -0,0 +1 @@
+Cookies specified as a dict were sent to every domain, not just the domain of the request, potentially exposing them on redirect. See `GHSA-fhpf-pp6p-55qc <https://github.com/twisted/treq/security/advisories/GHSA-fhpf-pp6p-55qc>`_.
--- a/src/treq/client.py
+++ b/src/treq/client.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import, division, print_function
 
 import mimetypes
+import sys
 import uuid
 
 from io import BytesIO
@@ -33,20 +34,70 @@ from treq._utils import default_reactor
 from treq.auth import add_auth
 from treq import multipart
 from treq.response import _Response
-from requests.cookies import cookiejar_from_dict, merge_cookies
+from requests.cookies import merge_cookies
 
 if _PY3:
     from urllib.parse import urlunparse, urlencode as _urlencode
 
     def urlencode(query, doseq):
         return _urlencode(query, doseq).encode('ascii')
-    from http.cookiejar import CookieJar
+    from http.cookiejar import CookieJar, Cookie
 else:
-    from cookielib import CookieJar
+    from cookielib import CookieJar, Cookie
     from urlparse import urlunparse
     from urllib import urlencode
 
 
+def _scoped_cookiejar_from_dict(url_object, cookie_dict):
+    """
+    Create a CookieJar from a dictionary whose cookies are all scoped to the
+    given URL's origin.
+
+    @note: This does not scope the cookies to any particular path, only the
+        host, port, and scheme of the given URL.
+    """
+    cookie_jar = CookieJar()
+    if cookie_dict is None:
+        return cookie_jar
+    for k, v in cookie_dict.items():
+        secure = url_object.scheme == 'https'
+        port_specified = not (
+            (url_object.scheme == "https" and url_object.port == 443)
+            or (url_object.scheme == "http" and url_object.port == 80)
+        )
+        port = str(url_object.port)
+        domain = url_object.hostname
+        netscape_domain = domain if b'.' in domain else domain + b'.local'
+
+        cookie_jar.set_cookie(
+            Cookie(
+                # Scoping
+                domain=netscape_domain,
+                port=port,
+                secure=secure,
+                port_specified=port_specified,
+
+                # Contents
+                name=k,
+                value=v,
+
+                # Constant/always-the-same stuff
+                version=0,
+                path="/",
+                expires=None,
+                discard=False,
+                comment=None,
+                comment_url=None,
+                rfc2109=False,
+                path_specified=False,
+                domain_specified=False,
+                domain_initial_dot=False,
+                rest=[],
+            )
+        )
+    return cookie_jar
+
+
 class _BodyBufferingProtocol(proxyForInterface(IProtocol)):
     def __init__(self, original, buffer, finished):
         self.original = original
@@ -102,7 +153,9 @@ class HTTPClient(object):
     def __init__(self, agent, cookiejar=None,
                  data_to_body_producer=IBodyProducer):
         self._agent = agent
-        self._cookiejar = cookiejar or cookiejar_from_dict({})
+        if cookiejar is None:
+            cookiejar = CookieJar()
+        self._cookiejar = cookiejar
         self._data_to_body_producer = data_to_body_producer
 
     def get(self, url, **kwargs):
@@ -216,7 +269,7 @@ class HTTPClient(object):
         cookies = kwargs.get('cookies', {})
 
         if not isinstance(cookies, CookieJar):
-            cookies = cookiejar_from_dict(cookies)
+            cookies = _scoped_cookiejar_from_dict(urlparse(url), cookies)
 
         cookies = merge_cookies(self._cookiejar, cookies)
         wrapped_agent = CookieAgent(self._agent, cookies)
--- a/src/treq/test/test_testing.py
+++ b/src/treq/test/test_testing.py
@@ -3,6 +3,7 @@ In-memory treq returns stubbed responses
 """
 from functools import partial
 from inspect import getmembers, isfunction
+from json import dumps
 
 from mock import ANY
 
@@ -35,6 +36,26 @@ class _StaticTestResource(Resource):
         return b"I'm a teapot"
 
 
+class _RedirectResource(Resource):
+    """
+    Resource that redirects to a different domain.
+    """
+    isLeaf = True
+
+    def render(self, request):
+        if b'redirected' not in request.uri:
+            request.redirect(b'https://example.org/redirected')
+        return dumps(
+            {
+                key.decode("charmap"): [
+                    value.decode("charmap")
+                    for value in values
+                ]
+                for key, values in
+                request.requestHeaders.getAllRawHeaders()}
+        ).encode("utf-8")
+
+
 class _NonResponsiveTestResource(Resource):
     """Resource that returns NOT_DONE_YET and never finishes the request"""
     isLeaf = True
@@ -245,7 +266,6 @@ class StubbingTests(TestCase):
         stub.flush()
         self.successResultOf(d)
 
-
 class HasHeadersTests(TestCase):
     """
     Tests for :obj:`HasHeaders`.
@@ -313,6 +333,25 @@ class HasHeadersTests(TestCase):
             reprOutput = "HasHeaders({'a': ['b']})"
         self.assertEqual(reprOutput, repr(HasHeaders({b'A': [b'b']})))
 
+    def test_different_domains(self):
+        """
+        Cookies manually specified as part of a dictionary are not relayed
+        through redirects.
+
+        (This is really more of a test for scoping of cookies within treq
+        itself, rather than just for testing.)
+        """
+        rsrc = _RedirectResource()
+        stub = StubTreq(rsrc)
+        d = stub.request(
+            "GET", "http://example.com/",
+            cookies={"not-across-redirect": "nope"}
+        )
+        resp = self.successResultOf(d)
+        received = self.successResultOf(resp.json())
+        self.assertNotIn('not-across-redirect', received.get('Cookie', [''])[0])
+
+
 
 class StringStubbingTests(TestCase):
     """
--- a/src/treq/test/test_treq_integration.py
+++ b/src/treq/test/test_treq_integration.py
@@ -29,6 +29,7 @@ def print_response(response):
         print('---')
         print(response.code)
         print(response.headers)
+        print(response.request.headers)
         text = yield treq.text_content(response)
         print(text)
         print('---')
openSUSE Build Service is sponsored by