Skip to content

Commit 3b7d81d

Browse files
miss-islingtonpicnixzambv
authored
[3.11] gh-136063: fix quadratic-complexity parsing in email.message._parseparam (GH-136072) (GH-140830)
(cherry picked from commit 680a5d0) Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 5dceb93 commit 3b7d81d

File tree

3 files changed

+39
-10
lines changed

3 files changed

+39
-10
lines changed

Lib/email/message.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,25 @@ def _parseparam(s):
7474
# RDM This might be a Header, so for now stringify it.
7575
s = ';' + str(s)
7676
plist = []
77-
while s[:1] == ';':
78-
s = s[1:]
79-
end = s.find(';')
80-
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
81-
end = s.find(';', end + 1)
77+
start = 0
78+
while s.find(';', start) == start:
79+
start += 1
80+
end = s.find(';', start)
81+
ind, diff = start, 0
82+
while end > 0:
83+
diff += s.count('"', ind, end) - s.count('\\"', ind, end)
84+
if diff % 2 == 0:
85+
break
86+
end, ind = ind, s.find(';', end + 1)
8287
if end < 0:
8388
end = len(s)
84-
f = s[:end]
85-
if '=' in f:
86-
i = f.index('=')
87-
f = f[:i].strip().lower() + '=' + f[i+1:].strip()
89+
i = s.find('=', start, end)
90+
if i == -1:
91+
f = s[start:end]
92+
else:
93+
f = s[start:i].rstrip().lower() + '=' + s[i+1:end].lstrip()
8894
plist.append(f.strip())
89-
s = s[end:]
95+
start = end
9096
return plist
9197

9298

Lib/test/test_email/test_email.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,27 @@ def test_get_param_with_quotes(self):
464464
"Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
465465
self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
466466

467+
def test_get_param_linear_complexity(self):
468+
# Ensure that email.message._parseparam() is fast.
469+
# See https://github.com/python/cpython/issues/136063.
470+
N = 100_000
471+
for s, r in [
472+
("", ""),
473+
("foo=bar", "foo=bar"),
474+
(" FOO = bar ", "foo=bar"),
475+
]:
476+
with self.subTest(s=s, r=r, N=N):
477+
src = f'{s};' * (N - 1) + s
478+
res = email.message._parseparam(src)
479+
self.assertEqual(len(res), N)
480+
self.assertEqual(len(set(res)), 1)
481+
self.assertEqual(res[0], r)
482+
483+
# This will be considered as a single parameter.
484+
malformed = 's="' + ';' * (N - 1)
485+
res = email.message._parseparam(malformed)
486+
self.assertEqual(res, [malformed])
487+
467488
def test_field_containment(self):
468489
msg = email.message_from_string('Header: exists')
469490
self.assertIn('header', msg)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`email.message`: ensure linear complexity for legacy HTTP parameters
2+
parsing. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)