diff --git a/changes/bd45773cff22d0cf547370e90918ba58.yaml b/changes/bd45773cff22d0cf547370e90918ba58.yaml new file mode 100644 index 00000000000..56ff78936af --- /dev/null +++ b/changes/bd45773cff22d0cf547370e90918ba58.yaml @@ -0,0 +1,5 @@ +--- +desc: Added a patch for Python ``http.cookies`` module to address CVE-2024-7592 exposure. +prs: [] +type: bug +... diff --git a/synapse/common.py b/synapse/common.py index b1640ccd18c..1796a4140c7 100644 --- a/synapse/common.py +++ b/synapse/common.py @@ -29,6 +29,8 @@ import contextlib import collections +import http.cookies + import yaml import regex @@ -38,6 +40,8 @@ import synapse.lib.structlog as s_structlog import synapse.vendor.cpython.lib.ipaddress as ipaddress +import synapse.vendor.cpython.lib.http.cookies as v_cookies + try: from yaml import CSafeLoader as Loader @@ -1218,6 +1222,17 @@ def trimText(text: str, n: int = 256, placeholder: str = '...') -> str: assert n > plen return f'{text[:mlen]}{placeholder}' +def _patch_http_cookies(): + ''' + Patch stdlib http.cookies._unquote from the 3.11.10 implementation if + the interpreter we are using is not patched for CVE-2024-7592. + ''' + if not hasattr(http.cookies, '_QuotePatt'): + return + http.cookies._unquote = v_cookies._unquote + +_patch_http_cookies() + # TODO: Switch back to using asyncio.wait_for when we are using py 3.12+ # This is a workaround for a race where asyncio.wait_for can end up # ignoring cancellation https://github.com/python/cpython/issues/86296 diff --git a/synapse/vendor/cpython/lib/http/__init__.py b/synapse/vendor/cpython/lib/http/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/synapse/vendor/cpython/lib/http/cookies.py b/synapse/vendor/cpython/lib/http/cookies.py new file mode 100644 index 00000000000..93243453dc4 --- /dev/null +++ b/synapse/vendor/cpython/lib/http/cookies.py @@ -0,0 +1,59 @@ +############################################################################## +# Taken from the cpython 3.11 source branch after the 3.11.10 release. +############################################################################## +#### +# Copyright 2000 by Timothy O'Malley +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software +# and its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Timothy O'Malley not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR +# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# + +# +# Import our required modules +# +import re + +_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub + +def _unquote_replace(m): + if m[1]: + return chr(int(m[1], 8)) + else: + return m[2] + +def _unquote(str): + # If there aren't any doublequotes, + # then there can't be any special characters. See RFC 2109. + if str is None or len(str) < 2: + return str + if str[0] != '"' or str[-1] != '"': + return str + + # We have to assume that we must decode this string. + # Down to work. + + # Remove the "s + str = str[1:-1] + + # Check for special sequences. Examples: + # \012 --> \n + # \" --> " + # + return _unquote_sub(_unquote_replace, str) diff --git a/synapse/vendor/cpython/lib/test/test_http_cookies.py b/synapse/vendor/cpython/lib/test/test_http_cookies.py new file mode 100644 index 00000000000..02d746f2c0e --- /dev/null +++ b/synapse/vendor/cpython/lib/test/test_http_cookies.py @@ -0,0 +1,49 @@ +############################################################################## +# Taken from the cpython 3.11 source branch after the 3.11.10 release. +# It has been modified for vendored imports and vendored test harness. +############################################################################## + +# Simple test suite for http/cookies.py + +from http import cookies + +# s_v_utils runs the monkeypatch +import synapse.vendor.utils as s_v_utils + +class CookieTests(s_v_utils.VendorTest): + + def test_unquote(self): + cases = [ + (r'a="b=\""', 'b="'), + (r'a="b=\\"', 'b=\\'), + (r'a="b=\="', 'b=='), + (r'a="b=\n"', 'b=n'), + (r'a="b=\042"', 'b="'), + (r'a="b=\134"', 'b=\\'), + (r'a="b=\377"', 'b=\xff'), + (r'a="b=\400"', 'b=400'), + (r'a="b=\42"', 'b=42'), + (r'a="b=\\042"', 'b=\\042'), + (r'a="b=\\134"', 'b=\\134'), + (r'a="b=\\\""', 'b=\\"'), + (r'a="b=\\\042"', 'b=\\"'), + (r'a="b=\134\""', 'b=\\"'), + (r'a="b=\134\042"', 'b=\\"'), + ] + for encoded, decoded in cases: + with self.subTest(encoded): + C = cookies.SimpleCookie() + C.load(encoded) + self.assertEqual(C['a'].value, decoded) + + def test_unquote_large(self): + n = 10**6 + for encoded in r'\\', r'\134': + with self.subTest(encoded): + data = 'a="b=' + encoded * n + ';"' + C = cookies.SimpleCookie() + C.load(data) + value = C['a'].value + self.assertEqual(value[:3], 'b=\\') + self.assertEqual(value[-2:], '\\;') + self.assertEqual(len(value), n + 3)