1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
From b4918d1e62a3f53cdba19d1b1af5938c7b6b1720 Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
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.
CVE: CVE-2024-52804
Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/d5ba4a1695fbf7c6a3e54313262639b198291533]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
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 bd32cd0..bb50786 100644
--- a/tornado/httputil.py
+++ b/tornado/httputil.py
@@ -1052,15 +1052,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.
"""
@@ -1081,30 +1086,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 0fad403..25faf66 100644
--- a/tornado/test/httputil_test.py
+++ b/tornado/test/httputil_test.py
@@ -519,3 +519,49 @@ class ParseCookieTest(unittest.TestCase):
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")
|