From 516a6a254814d2bc6a90290dfc44d77fdfb4050b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 18 Jun 2019 02:13:58 +0200 Subject: bpo-33529, email: Fix infinite loop in email header encoding (GH-12020) (GH-14162) (cherry picked from commit c1f5667be1e3ec5871560c677402c1252c6018a6) --- Lib/email/_header_value_parser.py | 23 ++++++++++++---------- Lib/test/test_email/test_headerregistry.py | 8 ++++---- Lib/test/test_email/test_policy.py | 8 ++++++++ .../2019-02-24-18-48-16.bpo-33529.wpNNBD.rst | 2 ++ 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2019-02-24-18-48-16.bpo-33529.wpNNBD.rst diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 1fb8cb448a..f42cde203c 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -2725,16 +2725,19 @@ def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset): lines.append(' ') # XXX We'll get an infinite loop here if maxlen is <= 7 continue - first_part = to_encode[:text_space] - ew = _ew.encode(first_part, charset=encode_as) - excess = len(ew) - remaining_space - if excess > 0: - # encode always chooses the shortest encoding, so this - # is guaranteed to fit at this point. - first_part = first_part[:-excess] - ew = _ew.encode(first_part) - lines[-1] += ew - to_encode = to_encode[len(first_part):] + + to_encode_word = to_encode[:text_space] + encoded_word = _ew.encode(to_encode_word, charset=encode_as) + excess = len(encoded_word) - remaining_space + while excess > 0: + # Since the chunk to encode is guaranteed to fit into less than 100 characters, + # shrinking it by one at a time shouldn't take long. + to_encode_word = to_encode_word[:-1] + encoded_word = _ew.encode(to_encode_word, charset=encode_as) + excess = len(encoded_word) - remaining_space + lines[-1] += encoded_word + to_encode = to_encode[len(to_encode_word):] + if to_encode: lines.append(' ') new_last_ew = len(lines[-1]) diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index 30ce0ba54e..d1007099f6 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -1643,10 +1643,10 @@ class TestFolding(TestHeaderBase): self.assertEqual( h.fold(policy=policy.default), 'X-Report-Abuse: =?utf-8?q?=3Chttps=3A//www=2Emailitapp=2E' - 'com/report=5F?=\n' - ' =?utf-8?q?abuse=2Ephp=3Fmid=3Dxxx-xxx-xxxx' - 'xxxxxxxxxxxxxxxxxxxx=3D=3D-xxx-?=\n' - ' =?utf-8?q?xx-xx=3E?=\n') + 'com/report=5Fabuse?=\n' + ' =?utf-8?q?=2Ephp=3Fmid=3Dxxx-xxx-xxxx' + 'xxxxxxxxxxxxxxxxxxxx=3D=3D-xxx-xx-xx?=\n' + ' =?utf-8?q?=3E?=\n') if __name__ == '__main__': diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py index 8fecb8a5fc..c2c437e6ac 100644 --- a/Lib/test/test_email/test_policy.py +++ b/Lib/test/test_email/test_policy.py @@ -237,6 +237,14 @@ class PolicyAPITests(unittest.TestCase): email.policy.EmailPolicy.header_factory) self.assertEqual(newpolicy.__dict__, {'raise_on_defect': True}) + def test_non_ascii_chars_do_not_cause_inf_loop(self): + policy = email.policy.default.clone(max_line_length=20) + actual = policy.fold('Subject', 'ą' * 12) + self.assertEqual( + actual, + 'Subject: \n' + + 12 * ' =?utf-8?q?=C4=85?=\n') + # XXX: Need subclassing tests. # For adding subclassed objects, make sure the usual rules apply (subclass # wins), but that the order still works (right overrides left). diff --git a/Misc/NEWS.d/next/Security/2019-02-24-18-48-16.bpo-33529.wpNNBD.rst b/Misc/NEWS.d/next/Security/2019-02-24-18-48-16.bpo-33529.wpNNBD.rst new file mode 100644 index 0000000000..84d16f5a56 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2019-02-24-18-48-16.bpo-33529.wpNNBD.rst @@ -0,0 +1,2 @@ +Prevent fold function used in email header encoding from entering infinite +loop when there are too many non-ASCII characters in a header. -- cgit v1.2.1