Sending email from Python using STARTTLS

user5415068 picture user5415068 · Nov 22, 2015 · Viewed 26.2k times · Source

I want to send emails with a Python script by using Python's smtplib.

The script should only send the email, if an encrypted connection to the server can be established. To encrypt the connection to port 587 I want to use STARTTLS.

Using some examples I have written the following code:

smtp_server = smtplib.SMTP(host, port=port)
context = ssl.create_default_context()    
smtp_server.starttls(context)
smtp_server.login(user, password)
smtp_server.send_message(msg)

msg, host, port, user, password are variables in my script. I have two questions:

  • Is the connection always encrypted or is it vulnerable to the STRIPTLS attack (https://en.wikipedia.org/wiki/STARTTLS).
  • Should I use the ehlo() method of the SMTP object? In some examples it is called explicitly before and after calling starttls(). On the other side in the documentation of smptlib it is written, that sendmail() will call it, if it is necessary.

[Edit]

@tintin explained, that ssl.create_default_context() can possibly lead to insecure connections. Thus I have changed the code using some examples in the following way:

_DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5')

smtp_server = smtplib.SMTP(host, port=port)

# only TLSv1 or higher
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3

context.set_ciphers(_DEFAULT_CIPHERS)
context.set_default_verify_paths()
context.verify_mode = ssl.CERT_REQUIRED

if smtp_server.starttls(context=context)[0] != 220:
    return False # cancel if connection is not encrypted
smtp_server.login(user, password)

For the cipher setting I used some code of a recent version of ssl.create_default_context(). Are these settings appropriate?

Note: In the code of my original question is one mistake. Here is the correct version of the concerned line:

smtp_server.starttls(context=context)

[\Edit]

Answer

tintin picture tintin · Nov 24, 2015

Is the connection always encrypted or is it vulnerable to the STRIPTLS attack (https://en.wikipedia.org/wiki/STARTTLS).

long story short: starttls can be stripped from smtplib <=py3.5.1rc1 <=py2.7.10 if you do not check response codes for .starttls()

  • explicitly calling .starttls() on smtp servers supporting it with a malicious MitM stripping your STARTTLS command and forging a non 220 response will NOT negotiate ssl, nor raise an exception and therefore leave your communication unencrypted - ergo it is vulnerable to striptls unless you manually verify that the response to .starttls()[0]==220 or the internal .sock got ssl wrapped.

    Here's a python 2.7.9 smtplib communication with an example similar to yours that failed to negotiate starttls by having the server or a MitM reply 999 NOSTARTTLS instead of the 200. No explicit check for the 200 response code in the client script, no exception due to a failed starttls attempt therefore mail transport not encrypted:

    220 xx ESMTP
    250-xx
    250-SIZE 20480000
    250-AUTH LOGIN
    250-STARTTLS
    250 HELP
    STARTTLS
    999 NOSTARTTLS
    mail FROM:<[email protected]> size=686
    250 OK
    rcpt TO:<[email protected]>
    250 OK
    data
    
  • explicitly calling .starttls() on smtp servers not supporting STARTTLS - or a MitM stripping this capability from the servers response - will raise SMTPNotSupportedError. see code below.

  • general note: encryption also depends on the configured cipherspec i.e. your SSLContext which in your case is created by ssl.create_default_context(). Note that it is totally valid to configure your SSLContext to allow cipherspecs that authenticate but do not encrypt (if offered/allowed by both server and client). E.g. TLS_RSA_WITH_NULL_SHA256.

    NULL-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=None Mac=SHA256

  • According to this answer python pre 2.7.9/3.4.3 does NOT attempt to enforce certificate validation for the default ssl context and therefore is vulnerable to ssl interception. Starting with Python 2.7.9/3.4.3 certificate validation is enforced for the default context. This also means, that you'll have to manually enable certificate validation for pre 2.7.9/3.4.3 (by creating a custom sslcontext) otherwise any untrusted certificate might be accepted.

Should I use the ehlo() method of the SMTP object? In some examples it is called explicitly before and after calling starttls(). On the other side in the documentation of smptlib it is written, that sendmail() will call it, if it is necessary.

  • .sendmail(), .send_message and .starttls() will implicitly call .ehlo_or_helo_if_needed() therefore there is no need to explicitly call it again. This is also

see source::smtplib::starttls (cpython, inofficial github) below:

def starttls(self, keyfile=None, certfile=None, context=None):
    """Puts the connection to the SMTP server into TLS mode.

    If there has been no previous EHLO or HELO command this session, this
    method tries ESMTP EHLO first.

    If the server supports TLS, this will encrypt the rest of the SMTP
    session. If you provide the keyfile and certfile parameters,
    the identity of the SMTP server and client can be checked. This,
    however, depends on whether the socket module really checks the
    certificates.

    This method may raise the following exceptions:

     SMTPHeloError            The server didn't reply properly to
                              the helo greeting.
    """
    self.ehlo_or_helo_if_needed()
    if not self.has_extn("starttls"):
        raise SMTPNotSupportedError(
            "STARTTLS extension not supported by server.")
    (resp, reply) = self.docmd("STARTTLS")
    if resp == 220:
        if not _have_ssl:
            raise RuntimeError("No SSL support included in this Python")
        if context is not None and keyfile is not None:
            raise ValueError("context and keyfile arguments are mutually "
                             "exclusive")
        if context is not None and certfile is not None:
            raise ValueError("context and certfile arguments are mutually "
                             "exclusive")
        if context is None:
            context = ssl._create_stdlib_context(certfile=certfile,
                                                 keyfile=keyfile)
        self.sock = context.wrap_socket(self.sock,
                                        server_hostname=self._host)
        self.file = None
        # RFC 3207:
        # The client MUST discard any knowledge obtained from
        # the server, such as the list of SMTP service extensions,
        # which was not obtained from the TLS negotiation itself.
        self.helo_resp = None
        self.ehlo_resp = None
        self.esmtp_features = {}
        self.does_esmtp = 0
    return (resp, reply)