Title: email.Message.as_string infinite loop
Type: security Stage: patch review
Components: email Versions: Python 3.9, Python 3.8, Python 3.7
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: barry, epicfaace, maxking, mytran, r.david.murray
Priority: normal Keywords: patch

Created on 2019-08-05 16:23 by mytran, last changed 2019-08-16 06:05 by maxking.

Pull Requests
URL Status Linked Edit
PR 15239 open epicfaace, 2019-08-12 23:04
Messages (11)
msg349055 - (view) Author: My Tran (mytran) Date: 2019-08-05 16:23
The following will hang the system until it runs out of memory. 

import email
import email.policy

text = """From:


eml = email.message_from_string(text, policy=email.policy.SMTPUTF8)
msg349196 - (view) Author: Ashwin Ramaswami (epicfaace) * Date: 2019-08-07 20:28
I can't reproduce this on 3.9:
msg349199 - (view) Author: Ashwin Ramaswami (epicfaace) * Date: 2019-08-07 20:37
I also can't reproduce this on 3.7:
msg349200 - (view) Author: My Tran (mytran) Date: 2019-08-07 20:51
Reproduced on 3.7.4

Looks like this started happening after this commit:
msg349202 - (view) Author: My Tran (mytran) Date: 2019-08-07 22:00
I looked at the job at and its running py3.6.
msg349277 - (view) Author: Abhilash Raj (maxking) * (Python committer) Date: 2019-08-09 08:23
This does look like a side-effect of the commit mentioned by mytran.

The issues seems to be that email._header_value_parser.get_unstructured wrongfully assumes that anything leading with '=?' would be a valid rfc 2047 encoded word.

This is a smaller test case to reproduce this bug:

from email._header_value_parser import get_unstructured
msg349358 - (view) Author: Abhilash Raj (maxking) * (Python committer) Date: 2019-08-10 20:32
Adding security label since this can cause DOS.
msg349505 - (view) Author: Ashwin Ramaswami (epicfaace) * Date: 2019-08-12 23:09
Oh, both the Travis links I sent actually ended up reproducing the bug.

I've made a PR that fixes with an even smaller test case:


It looks like this is caused because "aa" is thought to be an encoded word escape in -- thus, get_encoded_word fails, which ends up making get_unstructured go in an infinite loop.

My PR makes the parser parse "=?utf-8?q?somevalue?=aa" as "=?utf-8?q?somevalue?=aa". However, the existing test cases make sure it parses "=?utf-8?q?somevalue?=nowhitespace" as "somevaluenowhitespace". I'm not too familiar with RFC 2047, but why are "aa" and "nowhitespace" treated differently? Should they be?
msg349847 - (view) Author: Abhilash Raj (maxking) * (Python committer) Date: 2019-08-16 05:55
You have correctly identified that "=aa" is detected as a encoded word and causes the get_encoded_word to fail.

However, "=?utf-8?q?somevalue?=aa" should ideally get parsed as "somevalueaa" and not "=?utf-8?q?somevalue?=aa". This is because "=?utf-8?q?somevalue?=" is a valid encoded word, it is just not followed by an empty whitespace. 

modified   Lib/email/
@@ -1037,7 +1037,10 @@ def get_encoded_word(value):
         raise errors.HeaderParseError(
             "expected encoded word but found {}".format(value))
     remstr = ''.join(remainder)
-    if len(remstr) > 1 and remstr[0] in hexdigits and remstr[1] in hexdigits:
+    if (len(remstr) > 1 and
+        remstr[0] in hexdigits and
+        remstr[1] in hexdigits and
+        tok.count('?') < 2):
         # The ? after the CTE was followed by an encoded word escape (=XX).
         rest, *remainder = remstr.split('?=', 1)

This can be avoided by checking `?` occurs twice in the `tok`.

The 2nd bug, which needs a better test case, is that if the encoded_word is invalid, you will keep running into infinite loop, which you correctly fixed in your PR. However, the test case you used is more appropriate for the first issue.

You can fix both the issues, for which, you need to add a test case for 2nd issue and fix for the first issue.

Looking into the PR now.
msg349848 - (view) Author: Abhilash Raj (maxking) * (Python committer) Date: 2019-08-16 05:58
I meant, =aa is identified as encoded word escape
msg349849 - (view) Author: Abhilash Raj (maxking) * (Python committer) Date: 2019-08-16 06:05
Although, the 2nd bug I spoke of is kind of speculative, I haven't been able to find a test case which matches rfc2047_matcher but raises exception with get_encoded_word (after, ofcourse, the first bug is fixed), which the only way to cause an infinite loop.
Date User Action Args
2019-08-16 06:05:33maxkingsetmessages: + msg349849
2019-08-16 05:58:50maxkingsetmessages: + msg349848
2019-08-16 05:55:46maxkingsetmessages: + msg349847
2019-08-15 04:24:05epicfaacesetversions: + Python 3.9
2019-08-12 23:09:39epicfaacesetmessages: + msg349505
2019-08-12 23:04:05epicfaacesetkeywords: + patch
stage: patch review
pull_requests: + pull_request14960
2019-08-10 20:32:47maxkingsettype: security
messages: + msg349358
2019-08-09 08:23:33maxkingsetversions: + Python 3.8
2019-08-09 08:23:24maxkingsetmessages: + msg349277
2019-08-07 22:00:47mytransetmessages: + msg349202
2019-08-07 20:51:53mytransetmessages: + msg349200
2019-08-07 20:37:06epicfaacesetmessages: + msg349199
2019-08-07 20:28:29epicfaacesetnosy: + epicfaace
messages: + msg349196
2019-08-05 16:30:21xtreaksetnosy: + maxking
2019-08-05 16:23:23mytrancreate