|
| 1 | +From 04ac47b343b10f2182c4b3730d4be241b2397a4d Mon Sep 17 00:00:00 2001 |
| 2 | +From: Serhiy Storchaka <storchaka@gmail.com> |
| 3 | +Date: Fri, 16 Aug 2024 19:13:37 +0300 |
| 4 | +Subject: [PATCH 1/4] gh-123067: Fix quadratic complexity in parsing cookies |
| 5 | + with backslashes |
| 6 | + |
| 7 | +This fixes CVE-2024-7592. |
| 8 | +--- |
| 9 | + Lib/http/cookies.py | 34 ++++------------- |
| 10 | + Lib/test/test_http_cookies.py | 38 +++++++++++++++++++ |
| 11 | + ...-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst | 1 + |
| 12 | + 3 files changed, 47 insertions(+), 26 deletions(-) |
| 13 | + create mode 100644 Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 14 | + |
| 15 | +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py |
| 16 | +index 351faf428a20cd..11a67e8a2e008b 100644 |
| 17 | +--- a/Lib/http/cookies.py |
| 18 | ++++ b/Lib/http/cookies.py |
| 19 | +@@ -184,8 +184,12 @@ def _quote(str): |
| 20 | + return '"' + str.translate(_Translator) + '"' |
| 21 | + |
| 22 | + |
| 23 | +-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") |
| 24 | +-_QuotePatt = re.compile(r"[\\].") |
| 25 | ++_unquote_re = re.compile(r'\\(?:([0-3][0-7][0-7])|(["\\]))') |
| 26 | ++def _unquote_replace(m): |
| 27 | ++ if m[1]: |
| 28 | ++ return chr(int(m[1], 8)) |
| 29 | ++ else: |
| 30 | ++ return m[2] |
| 31 | + |
| 32 | + def _unquote(str): |
| 33 | + # If there aren't any doublequotes, |
| 34 | +@@ -205,30 +209,8 @@ def _unquote(str): |
| 35 | + # \012 --> \n |
| 36 | + # \" --> " |
| 37 | + # |
| 38 | +- i = 0 |
| 39 | +- n = len(str) |
| 40 | +- res = [] |
| 41 | +- while 0 <= i < n: |
| 42 | +- o_match = _OctalPatt.search(str, i) |
| 43 | +- q_match = _QuotePatt.search(str, i) |
| 44 | +- if not o_match and not q_match: # Neither matched |
| 45 | +- res.append(str[i:]) |
| 46 | +- break |
| 47 | +- # else: |
| 48 | +- j = k = -1 |
| 49 | +- if o_match: |
| 50 | +- j = o_match.start(0) |
| 51 | +- if q_match: |
| 52 | +- k = q_match.start(0) |
| 53 | +- if q_match and (not o_match or k < j): # QuotePatt matched |
| 54 | +- res.append(str[i:k]) |
| 55 | +- res.append(str[k+1]) |
| 56 | +- i = k + 2 |
| 57 | +- else: # OctalPatt matched |
| 58 | +- res.append(str[i:j]) |
| 59 | +- res.append(chr(int(str[j+1:j+4], 8))) |
| 60 | +- i = j + 4 |
| 61 | +- return _nulljoin(res) |
| 62 | ++ |
| 63 | ++ return _unquote_re.sub(_unquote_replace, str) |
| 64 | + |
| 65 | + # The _getdate() routine is used to set the expiration time in the cookie's HTTP |
| 66 | + # header. By default, _getdate() returns the current time in the appropriate |
| 67 | +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py |
| 68 | +index 925c8697f60de6..13b526d49b0856 100644 |
| 69 | +--- a/Lib/test/test_http_cookies.py |
| 70 | ++++ b/Lib/test/test_http_cookies.py |
| 71 | +@@ -5,6 +5,7 @@ |
| 72 | + import unittest |
| 73 | + from http import cookies |
| 74 | + import pickle |
| 75 | ++from test import support |
| 76 | + |
| 77 | + |
| 78 | + class CookieTests(unittest.TestCase): |
| 79 | +@@ -58,6 +59,43 @@ def test_basic(self): |
| 80 | + for k, v in sorted(case['dict'].items()): |
| 81 | + self.assertEqual(C[k].value, v) |
| 82 | + |
| 83 | ++ def test_unquote(self): |
| 84 | ++ cases = [ |
| 85 | ++ (r'a="b=\""', 'b="'), |
| 86 | ++ (r'a="b=\\"', 'b=\\'), |
| 87 | ++ (r'a="b=\="', 'b=\\='), |
| 88 | ++ (r'a="b=\n"', 'b=\\n'), |
| 89 | ++ (r'a="b=\042"', 'b="'), |
| 90 | ++ (r'a="b=\134"', 'b=\\'), |
| 91 | ++ (r'a="b=\377"', 'b=\xff'), |
| 92 | ++ (r'a="b=\400"', 'b=\\400'), |
| 93 | ++ (r'a="b=\42"', 'b=\\42'), |
| 94 | ++ (r'a="b=\\042"', 'b=\\042'), |
| 95 | ++ (r'a="b=\\134"', 'b=\\134'), |
| 96 | ++ (r'a="b=\\\""', 'b=\\"'), |
| 97 | ++ (r'a="b=\\\042"', 'b=\\"'), |
| 98 | ++ (r'a="b=\134\""', 'b=\\"'), |
| 99 | ++ (r'a="b=\134\042"', 'b=\\"'), |
| 100 | ++ ] |
| 101 | ++ for encoded, decoded in cases: |
| 102 | ++ with self.subTest(encoded): |
| 103 | ++ C = cookies.SimpleCookie() |
| 104 | ++ C.load(encoded) |
| 105 | ++ self.assertEqual(C['a'].value, decoded) |
| 106 | ++ |
| 107 | ++ @support.requires_resource('cpu') |
| 108 | ++ def test_unquote_large(self): |
| 109 | ++ n = 10**6 |
| 110 | ++ for encoded in r'\\', r'\134': |
| 111 | ++ with self.subTest(encoded): |
| 112 | ++ data = 'a="b=' + encoded*n + ';"' |
| 113 | ++ C = cookies.SimpleCookie() |
| 114 | ++ C.load(data) |
| 115 | ++ value = C['a'].value |
| 116 | ++ self.assertEqual(value[:3], 'b=\\') |
| 117 | ++ self.assertEqual(value[-2:], '\\;') |
| 118 | ++ self.assertEqual(len(value), n + 3) |
| 119 | ++ |
| 120 | + def test_load(self): |
| 121 | + C = cookies.SimpleCookie() |
| 122 | + C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme') |
| 123 | +diff --git a/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 124 | +new file mode 100644 |
| 125 | +index 00000000000000..158b938a65a2d4 |
| 126 | +--- /dev/null |
| 127 | ++++ b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 128 | +@@ -0,0 +1 @@ |
| 129 | ++Fix quadratic complexity in parsing cookies with backslashes. |
| 130 | + |
| 131 | +From ab87c992c2d4cd28560178048915bc9636d6566e Mon Sep 17 00:00:00 2001 |
| 132 | +From: Serhiy Storchaka <storchaka@gmail.com> |
| 133 | +Date: Fri, 16 Aug 2024 19:38:20 +0300 |
| 134 | +Subject: [PATCH 2/4] Restore the current behavior for backslash-escaping. |
| 135 | + |
| 136 | +--- |
| 137 | + Lib/http/cookies.py | 2 +- |
| 138 | + Lib/test/test_http_cookies.py | 8 ++++---- |
| 139 | + 2 files changed, 5 insertions(+), 5 deletions(-) |
| 140 | + |
| 141 | +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py |
| 142 | +index 11a67e8a2e008b..464abeb0fb253a 100644 |
| 143 | +--- a/Lib/http/cookies.py |
| 144 | ++++ b/Lib/http/cookies.py |
| 145 | +@@ -184,7 +184,7 @@ def _quote(str): |
| 146 | + return '"' + str.translate(_Translator) + '"' |
| 147 | + |
| 148 | + |
| 149 | +-_unquote_re = re.compile(r'\\(?:([0-3][0-7][0-7])|(["\\]))') |
| 150 | ++_unquote_re = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))') |
| 151 | + def _unquote_replace(m): |
| 152 | + if m[1]: |
| 153 | + return chr(int(m[1], 8)) |
| 154 | +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py |
| 155 | +index 13b526d49b0856..8879902a6e2f41 100644 |
| 156 | +--- a/Lib/test/test_http_cookies.py |
| 157 | ++++ b/Lib/test/test_http_cookies.py |
| 158 | +@@ -63,13 +63,13 @@ def test_unquote(self): |
| 159 | + cases = [ |
| 160 | + (r'a="b=\""', 'b="'), |
| 161 | + (r'a="b=\\"', 'b=\\'), |
| 162 | +- (r'a="b=\="', 'b=\\='), |
| 163 | +- (r'a="b=\n"', 'b=\\n'), |
| 164 | ++ (r'a="b=\="', 'b=='), |
| 165 | ++ (r'a="b=\n"', 'b=n'), |
| 166 | + (r'a="b=\042"', 'b="'), |
| 167 | + (r'a="b=\134"', 'b=\\'), |
| 168 | + (r'a="b=\377"', 'b=\xff'), |
| 169 | +- (r'a="b=\400"', 'b=\\400'), |
| 170 | +- (r'a="b=\42"', 'b=\\42'), |
| 171 | ++ (r'a="b=\400"', 'b=400'), |
| 172 | ++ (r'a="b=\42"', 'b=42'), |
| 173 | + (r'a="b=\\042"', 'b=\\042'), |
| 174 | + (r'a="b=\\134"', 'b=\\134'), |
| 175 | + (r'a="b=\\\""', 'b=\\"'), |
| 176 | + |
| 177 | +From 1fe24921da4c6c547da82e11c9703f3588dc5fab Mon Sep 17 00:00:00 2001 |
| 178 | +From: Serhiy Storchaka <storchaka@gmail.com> |
| 179 | +Date: Sat, 17 Aug 2024 12:40:11 +0300 |
| 180 | +Subject: [PATCH 3/4] Cache the sub() method, not the compiled pattern object. |
| 181 | + |
| 182 | +--- |
| 183 | + Lib/http/cookies.py | 6 +++--- |
| 184 | + 1 file changed, 3 insertions(+), 3 deletions(-) |
| 185 | + |
| 186 | +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py |
| 187 | +index 464abeb0fb253a..6b9ed24ad8ec78 100644 |
| 188 | +--- a/Lib/http/cookies.py |
| 189 | ++++ b/Lib/http/cookies.py |
| 190 | +@@ -184,7 +184,8 @@ def _quote(str): |
| 191 | + return '"' + str.translate(_Translator) + '"' |
| 192 | + |
| 193 | + |
| 194 | +-_unquote_re = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))') |
| 195 | ++_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub |
| 196 | ++ |
| 197 | + def _unquote_replace(m): |
| 198 | + if m[1]: |
| 199 | + return chr(int(m[1], 8)) |
| 200 | +@@ -209,8 +210,7 @@ def _unquote(str): |
| 201 | + # \012 --> \n |
| 202 | + # \" --> " |
| 203 | + # |
| 204 | +- |
| 205 | +- return _unquote_re.sub(_unquote_replace, str) |
| 206 | ++ return _unquote_sub(_unquote_replace, str) |
| 207 | + |
| 208 | + # The _getdate() routine is used to set the expiration time in the cookie's HTTP |
| 209 | + # header. By default, _getdate() returns the current time in the appropriate |
| 210 | + |
| 211 | +From 8256ed2228137c87d4b20747db84a9cdf0fa1d34 Mon Sep 17 00:00:00 2001 |
| 212 | +From: Serhiy Storchaka <storchaka@gmail.com> |
| 213 | +Date: Sat, 17 Aug 2024 13:08:20 +0300 |
| 214 | +Subject: [PATCH 4/4] Add a reference to the module in NEWS. |
| 215 | + |
| 216 | +--- |
| 217 | + .../next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst | 2 +- |
| 218 | + 1 file changed, 1 insertion(+), 1 deletion(-) |
| 219 | + |
| 220 | +diff --git a/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 221 | +index 158b938a65a2d4..6a234561fe31a3 100644 |
| 222 | +--- a/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 223 | ++++ b/Misc/NEWS.d/next/Library/2024-08-16-19-13-21.gh-issue-123067.Nx9O4R.rst |
| 224 | +@@ -1 +1 @@ |
| 225 | +-Fix quadratic complexity in parsing cookies with backslashes. |
| 226 | ++Fix quadratic complexity in parsing ``"``-quoted cookie values with backslashes by :mod:`http.cookies`. |
0 commit comments