Skip to content

Commit 1da6022

Browse files
authored
Merge pull request from GHSA-fhpf-pp6p-55qc
scope cookies by default
2 parents d89d553 + b1c33ca commit 1da6022

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

changelog.d/339.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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>`_.

src/treq/client.py

+56-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import uuid
44
import warnings
55
from collections.abc import Mapping
6-
from http.cookiejar import CookieJar
6+
from http.cookiejar import CookieJar, Cookie
77
from urllib.parse import quote_plus, urlencode as _urlencode
88

99
from twisted.internet.interfaces import IProtocol
@@ -30,7 +30,7 @@
3030
from treq.auth import add_auth
3131
from treq import multipart
3232
from treq.response import _Response
33-
from requests.cookies import cookiejar_from_dict, merge_cookies
33+
from requests.cookies import merge_cookies
3434

3535

3636
_NOTHING = object()
@@ -43,6 +43,56 @@ def urlencode(query, doseq):
4343
return s
4444

4545

46+
def _scoped_cookiejar_from_dict(url_object, cookie_dict):
47+
"""
48+
Create a CookieJar from a dictionary whose cookies are all scoped to the
49+
given URL's origin.
50+
51+
@note: This does not scope the cookies to any particular path, only the
52+
host, port, and scheme of the given URL.
53+
"""
54+
cookie_jar = CookieJar()
55+
if cookie_dict is None:
56+
return cookie_jar
57+
for k, v in cookie_dict.items():
58+
secure = url_object.scheme == 'https'
59+
port_specified = not (
60+
(url_object.scheme == "https" and url_object.port == 443)
61+
or (url_object.scheme == "http" and url_object.port == 80)
62+
)
63+
port = str(url_object.port)
64+
domain = url_object.host
65+
netscape_domain = domain if '.' in domain else domain + '.local'
66+
67+
cookie_jar.set_cookie(
68+
Cookie(
69+
# Scoping
70+
domain=netscape_domain,
71+
port=port,
72+
secure=secure,
73+
port_specified=port_specified,
74+
75+
# Contents
76+
name=k,
77+
value=v,
78+
79+
# Constant/always-the-same stuff
80+
version=0,
81+
path="/",
82+
expires=None,
83+
discard=False,
84+
comment=None,
85+
comment_url=None,
86+
rfc2109=False,
87+
path_specified=False,
88+
domain_specified=False,
89+
domain_initial_dot=False,
90+
rest=[],
91+
)
92+
)
93+
return cookie_jar
94+
95+
4696
class _BodyBufferingProtocol(proxyForInterface(IProtocol)):
4797
def __init__(self, original, buffer, finished):
4898
self.original = original
@@ -98,7 +148,9 @@ class HTTPClient:
98148
def __init__(self, agent, cookiejar=None,
99149
data_to_body_producer=IBodyProducer):
100150
self._agent = agent
101-
self._cookiejar = cookiejar or cookiejar_from_dict({})
151+
if cookiejar is None:
152+
cookiejar = CookieJar()
153+
self._cookiejar = cookiejar
102154
self._data_to_body_producer = data_to_body_producer
103155

104156
def get(self, url, **kwargs):
@@ -195,7 +247,7 @@ def request(
195247
headers.setRawHeaders(b'Content-Type', [contentType])
196248

197249
if not isinstance(cookies, CookieJar):
198-
cookies = cookiejar_from_dict(cookies)
250+
cookies = _scoped_cookiejar_from_dict(parsed_url, cookies)
199251

200252
cookies = merge_cookies(self._cookiejar, cookies)
201253
wrapped_agent = CookieAgent(self._agent, cookies)

src/treq/test/test_testing.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from functools import partial
55
from inspect import getmembers, isfunction
6+
from json import dumps
67

78
from unittest.mock import ANY
89

@@ -32,6 +33,26 @@ def render(self, request):
3233
return b"I'm a teapot"
3334

3435

36+
class _RedirectResource(Resource):
37+
"""
38+
Resource that redirects to a different domain.
39+
"""
40+
isLeaf = True
41+
42+
def render(self, request):
43+
if b'redirected' not in request.uri:
44+
request.redirect(b'https://example.org/redirected')
45+
return dumps(
46+
{
47+
key.decode("charmap"): [
48+
value.decode("charmap")
49+
for value in values
50+
]
51+
for key, values in
52+
request.requestHeaders.getAllRawHeaders()}
53+
).encode("utf-8")
54+
55+
3556
class _NonResponsiveTestResource(Resource):
3657
"""Resource that returns NOT_DONE_YET and never finishes the request"""
3758
isLeaf = True
@@ -272,8 +293,10 @@ def test_handles_successful_asynchronous_requests_with_streaming(self):
272293

273294
def test_session_persistence_between_requests(self):
274295
"""
275-
Calling request.getSession() in the wrapped resource will return
276-
a session with the same ID, until the sessions are cleaned.
296+
Calling request.getSession() in the wrapped resource will return a
297+
session with the same ID, until the sessions are cleaned; in other
298+
words, cookies are propagated between requests when the result of
299+
C{response.cookies()} is passed to the next request.
277300
"""
278301
rsrc = _SessionIdTestResource()
279302
stub = StubTreq(rsrc)
@@ -304,6 +327,24 @@ def test_session_persistence_between_requests(self):
304327
sid_4 = self.successResultOf(resp.content())
305328
self.assertEqual(sid_3, sid_4)
306329

330+
def test_different_domains(self):
331+
"""
332+
Cookies manually specified as part of a dictionary are not relayed
333+
through redirects.
334+
335+
(This is really more of a test for scoping of cookies within treq
336+
itself, rather than just for testing.)
337+
"""
338+
rsrc = _RedirectResource()
339+
stub = StubTreq(rsrc)
340+
d = stub.request(
341+
"GET", "http://example.com/",
342+
cookies={"not-across-redirect": "nope"}
343+
)
344+
resp = self.successResultOf(d)
345+
received = self.successResultOf(resp.json())
346+
self.assertNotIn('not-across-redirect', received.get('Cookie', [''])[0])
347+
307348

308349
class HasHeadersTests(TestCase):
309350
"""

src/treq/test/test_treq_integration.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def print_response(response):
2929
print('---')
3030
print(response.code)
3131
print(response.headers)
32+
print(response.request.headers)
3233
text = yield treq.text_content(response)
3334
print(text)
3435
print('---')

0 commit comments

Comments
 (0)