diff --git a/CVE-2024-52804-avoid-quadratic-cookie-parsing.patch b/CVE-2024-52804-avoid-quadratic-cookie-parsing.patch new file mode 100644 index 0000000..349a7ec --- /dev/null +++ b/CVE-2024-52804-avoid-quadratic-cookie-parsing.patch @@ -0,0 +1,138 @@ +From d5ba4a1695fbf7c6a3e54313262639b198291533 Mon Sep 17 00:00:00 2001 +From: Ben Darnell +Date: Thu, 21 Nov 2024 14:48:05 -0500 +Subject: [PATCH] httputil: Fix quadratic performance of cookie parsing + +Maliciously-crafted cookies can cause Tornado to +spend an unreasonable amount of CPU time and block +the event loop. + +This change replaces the quadratic algorithm with +a more efficient one. The implementation is copied +from the Python 3.13 standard library (the +previous one was from Python 3.5). + +Fixes CVE-2024-52804 +See CVE-2024-7592 for a similar vulnerability in cpython. + +Thanks to github.com/kexinoh for the report. +--- + tornado/httputil.py | 38 ++++++++--------------------- + tornado/test/httputil_test.py | 46 +++++++++++++++++++++++++++++++++++ + 2 files changed, 56 insertions(+), 28 deletions(-) + +diff --git a/tornado/httputil.py b/tornado/httputil.py +index 9ce992d82b..ebdc8059c1 100644 +--- a/tornado/httputil.py ++++ b/tornado/httputil.py +@@ -1057,15 +1057,20 @@ def qs_to_qsl(qs: Dict[str, List[AnyStr]]) -> Iterable[Tuple[str, AnyStr]]: + yield (k, v) + + +-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") +-_QuotePatt = re.compile(r"[\\].") +-_nulljoin = "".join ++_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub ++ ++ ++def _unquote_replace(m: re.Match) -> str: ++ if m[1]: ++ return chr(int(m[1], 8)) ++ else: ++ return m[2] + + + def _unquote_cookie(s: str) -> str: + """Handle double quotes and escaping in cookie values. + +- This method is copied verbatim from the Python 3.5 standard ++ This method is copied verbatim from the Python 3.13 standard + library (http.cookies._unquote) so we don't have to depend on + non-public interfaces. + """ +@@ -1086,30 +1091,7 @@ def _unquote_cookie(s: str) -> str: + # \012 --> \n + # \" --> " + # +- i = 0 +- n = len(s) +- res = [] +- while 0 <= i < n: +- o_match = _OctalPatt.search(s, i) +- q_match = _QuotePatt.search(s, i) +- if not o_match and not q_match: # Neither matched +- res.append(s[i:]) +- break +- # else: +- j = k = -1 +- if o_match: +- j = o_match.start(0) +- if q_match: +- k = q_match.start(0) +- if q_match and (not o_match or k < j): # QuotePatt matched +- res.append(s[i:k]) +- res.append(s[k + 1]) +- i = k + 2 +- else: # OctalPatt matched +- res.append(s[i:j]) +- res.append(chr(int(s[j + 1 : j + 4], 8))) +- i = j + 4 +- return _nulljoin(res) ++ return _unquote_sub(_unquote_replace, s) + + + def parse_cookie(cookie: str) -> Dict[str, str]: +diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py +index 6d618839e0..975900aa9c 100644 +--- a/tornado/test/httputil_test.py ++++ b/tornado/test/httputil_test.py +@@ -560,3 +560,49 @@ def test_invalid_cookies(self): + self.assertEqual( + parse_cookie(" = b ; ; = ; c = ; "), {"": "b", "c": ""} + ) ++ ++ def test_unquote(self): ++ # Copied from ++ # https://github.com/python/cpython/blob/dc7a2b6522ec7af41282bc34f405bee9b306d611/Lib/test/test_http_cookies.py#L62 ++ 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 = parse_cookie(encoded) ++ self.assertEqual(c["a"], decoded) ++ ++ def test_unquote_large(self): ++ # Adapted from ++ # https://github.com/python/cpython/blob/dc7a2b6522ec7af41282bc34f405bee9b306d611/Lib/test/test_http_cookies.py#L87 ++ # Modified from that test because we handle semicolons differently from the stdlib. ++ # ++ # This is a performance regression test: prior to improvements in Tornado 6.4.2, this test ++ # would take over a minute with n= 100k. Now it runs in tens of milliseconds. ++ n = 100000 ++ for encoded in r"\\", r"\134": ++ with self.subTest(encoded): ++ start = time.time() ++ data = 'a="b=' + encoded * n + '"' ++ value = parse_cookie(data)["a"] ++ end = time.time() ++ self.assertEqual(value[:3], "b=\\") ++ self.assertEqual(value[-3:], "\\\\\\") ++ self.assertEqual(len(value), n + 2) ++ ++ # Very loose performance check to avoid false positives ++ self.assertLess(end - start, 1, "Test took too long") diff --git a/python-tornado6.changes b/python-tornado6.changes index 8ff1b18..bf2e9f0 100644 --- a/python-tornado6.changes +++ b/python-tornado6.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Nov 27 04:23:13 UTC 2024 - Steve Kowalik + +- Add patch CVE-2024-52804-avoid-quadratic-cookie-parsing.patch: + * Avoid quadratic performance of cookie parsing. + (CVE-2024-52804, bsc#1233668) + ------------------------------------------------------------------- Thu Jan 11 13:28:34 UTC 2024 - Daniel Garcia diff --git a/python-tornado6.spec b/python-tornado6.spec index 2b35ce5..2ab9869 100644 --- a/python-tornado6.spec +++ b/python-tornado6.spec @@ -30,6 +30,9 @@ Source99: python-tornado6-rpmlintrc Patch0: ignore-resourcewarning-doctests.patch # PATCH-FIX-OPENSUSE openssl-3.2.patch gh#tornadoweb/tornado#3355 Patch1: openssl-3.2.patch +# PATCH-FIX-UPSTREAM CVE-2024-52804 bsc#1233668 +# gh#tornadoweb/tornado#d5ba4a1695fbf7c6a3e54313262639b198291533 +Patch2: CVE-2024-52804-avoid-quadratic-cookie-parsing.patch BuildRequires: %{python_module base >= 3.8} BuildRequires: %{python_module devel} BuildRequires: %{python_module pip}