| From 3b7d81da078d48f72d50aa8c2bf06a97d20bd913 Mon Sep 17 00:00:00 2001 |
| From: "Miss Islington (bot)" |
| <31488909+miss-islington@users.noreply.github.com> |
| Date: Fri, 31 Oct 2025 18:29:53 +0100 |
| Subject: [PATCH] [3.11] gh-136063: fix quadratic-complexity parsing in |
| `email.message._parseparam` (GH-136072) (GH-140830) |
| MIME-Version: 1.0 |
| Content-Type: text/plain; charset=UTF-8 |
| Content-Transfer-Encoding: 8bit |
| |
| (cherry picked from commit 680a5d070f59798bb88a1bb6eb027482b8d85c34) |
| |
| Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> |
| Co-authored-by: Łukasz Langa <lukasz@langa.pl> |
| --- |
| Lib/email/message.py | 26 ++++++++++++------- |
| Lib/test/test_email/test_email.py | 21 +++++++++++++++ |
| ...-06-28-13-23-53.gh-issue-136063.aGk0Jv.rst | 2 ++ |
| 3 files changed, 39 insertions(+), 10 deletions(-) |
| create mode 100644 Misc/NEWS.d/next/Security/2025-06-28-13-23-53.gh-issue-136063.aGk0Jv.rst |
| |
| diff --git a/Lib/email/message.py b/Lib/email/message.py |
| index 492a6b9a4309fa..6a9903f9c8e842 100644 |
| --- a/Lib/email/message.py |
| +++ b/Lib/email/message.py |
| @@ -74,19 +74,25 @@ def _parseparam(s): |
| # RDM This might be a Header, so for now stringify it. |
| s = ';' + str(s) |
| plist = [] |
| - while s[:1] == ';': |
| - s = s[1:] |
| - end = s.find(';') |
| - while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: |
| - end = s.find(';', end + 1) |
| + start = 0 |
| + while s.find(';', start) == start: |
| + start += 1 |
| + end = s.find(';', start) |
| + ind, diff = start, 0 |
| + while end > 0: |
| + diff += s.count('"', ind, end) - s.count('\\"', ind, end) |
| + if diff % 2 == 0: |
| + break |
| + end, ind = ind, s.find(';', end + 1) |
| if end < 0: |
| end = len(s) |
| - f = s[:end] |
| - if '=' in f: |
| - i = f.index('=') |
| - f = f[:i].strip().lower() + '=' + f[i+1:].strip() |
| + i = s.find('=', start, end) |
| + if i == -1: |
| + f = s[start:end] |
| + else: |
| + f = s[start:i].rstrip().lower() + '=' + s[i+1:end].lstrip() |
| plist.append(f.strip()) |
| - s = s[end:] |
| + start = end |
| return plist |
| |
| |
| diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py |
| index ad60ed3a7591c0..431d362718ada7 100644 |
| --- a/Lib/test/test_email/test_email.py |
| +++ b/Lib/test/test_email/test_email.py |
| @@ -464,6 +464,27 @@ def test_get_param_with_quotes(self): |
| "Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"") |
| self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz') |
| |
| + def test_get_param_linear_complexity(self): |
| + # Ensure that email.message._parseparam() is fast. |
| + # See https://github.com/python/cpython/issues/136063. |
| + N = 100_000 |
| + for s, r in [ |
| + ("", ""), |
| + ("foo=bar", "foo=bar"), |
| + (" FOO = bar ", "foo=bar"), |
| + ]: |
| + with self.subTest(s=s, r=r, N=N): |
| + src = f'{s};' * (N - 1) + s |
| + res = email.message._parseparam(src) |
| + self.assertEqual(len(res), N) |
| + self.assertEqual(len(set(res)), 1) |
| + self.assertEqual(res[0], r) |
| + |
| + # This will be considered as a single parameter. |
| + malformed = 's="' + ';' * (N - 1) |
| + res = email.message._parseparam(malformed) |
| + self.assertEqual(res, [malformed]) |
| + |
| def test_field_containment(self): |
| msg = email.message_from_string('Header: exists') |
| self.assertIn('header', msg) |
| diff --git a/Misc/NEWS.d/next/Security/2025-06-28-13-23-53.gh-issue-136063.aGk0Jv.rst b/Misc/NEWS.d/next/Security/2025-06-28-13-23-53.gh-issue-136063.aGk0Jv.rst |
| new file mode 100644 |
| index 00000000000000..940a3ad5a72f68 |
| --- /dev/null |
| +++ b/Misc/NEWS.d/next/Security/2025-06-28-13-23-53.gh-issue-136063.aGk0Jv.rst |
| @@ -0,0 +1,2 @@ |
| +:mod:`email.message`: ensure linear complexity for legacy HTTP parameters |
| +parsing. Patch by Bénédikt Tran. |