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('---')