使用STARTTLS从Python发送邮件

13 投票
1 回答
48549 浏览
提问于 2025-05-10 20:37

我想用Python脚本通过Python的smtplib发送电子邮件。

这个脚本应该只有在能够建立与服务器的加密连接时才发送邮件。为了加密到587端口的连接,我想使用STARTTLS。

参考了一些例子,我写了以下代码:

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是我脚本中的变量。我有两个问题:

  • 这个连接总是加密的吗?还是说它可能会受到STRIPTLS攻击的威胁?(https://en.wikipedia.org/wiki/STARTTLS
  • 我应该使用SMTP对象的ehlo()方法吗?在一些例子中,它在调用starttls()之前和之后都被明确调用。而在smtplib的文档中写着,如果需要,sendmail()会自动调用它。

[编辑]

@tintin解释说,ssl.create_default_context()可能会导致不安全的连接。因此,我根据一些例子修改了代码,如下所示:

_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)

在密码设置方面,我使用了ssl.create_default_context()的一个较新版本的代码。这些设置合适吗?

注意:我原始问题中的代码有一个错误。这里是相关行的正确版本:

smtp_server.starttls(context=context)

[\编辑]

相关文章:

  • 暂无相关问题
暂无标签

1 个回答

16

连接总是加密的吗,还是会受到STRIPTLS攻击的影响?(https://en.wikipedia.org/wiki/STARTTLS

简单来说:如果你不检查.starttls()的响应代码,smtplib版本小于等于py3.5.1rc1和py2.7.10的情况下,STARTTLS可能会被剥离。

  • 如果你在支持STARTTLS的SMTP服务器上明确调用.starttls(),而中间人攻击者(MitM)剥离了你的STARTTLS命令,并伪造了一个非220的响应,这不会协商SSL,也不会抛出异常,因此你的通信将不会加密——也就是说,它容易受到STRIPTLS攻击,除非你手动验证.starttls()[0]==220的响应,或者内部的.sock已经被SSL包裹。

    下面是一个Python 2.7.9的smtplib通信示例,类似于你的情况,由于服务器或中间人回复999 NOSTARTTLS而未能协商STARTTLS,而不是200。客户端脚本中没有明确检查200响应代码,因此由于STARTTLS尝试失败,没有异常,邮件传输没有加密:

    220 xx ESMTP
    250-xx
    250-SIZE 20480000
    250-AUTH LOGIN
    250-STARTTLS
    250 HELP
    STARTTLS
    999 NOSTARTTLS
    mail FROM:<a@b.com> size=686
    250 OK
    rcpt TO:<a@b.com>
    250 OK
    data
    
  • 在不支持STARTTLS的SMTP服务器上明确调用.starttls(),或者中间人剥离了服务器的这个能力,会引发SMTPNotSupportedError。请看下面的代码。

  • 一般来说,加密还依赖于配置的加密套件,也就是你的SSLContext,在你的情况下是通过ssl.create_default_context()创建的。请注意,配置SSLContext以允许认证但不加密的加密套件是完全有效的(如果服务器和客户端都允许)。例如TLS_RSA_WITH_NULL_SHA256

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

  • 根据这个回答,Python 2.7.9/3.4.3之前的版本会强制验证默认SSL上下文的证书,因此容易受到SSL拦截。从Python 2.7.9/3.4.3开始,默认上下文会强制进行证书验证。这也意味着,对于 2.7.9/3.4.3之前的版本,你需要手动启用证书验证(通过创建自定义的sslcontext),否则任何不受信任的证书都可能被接受。

我应该使用SMTP对象的ehlo()方法吗?在一些示例中,它在调用starttls()之前和之后被明确调用。另一方面,在smtplib的文档中写着,如果有必要,sendmail()会调用它。

  • .sendmail().send_message.starttls()会隐式调用.ehlo_or_helo_if_needed(),因此不需要再次明确调用它。这也是

请参见源代码::smtplib::starttls (cpython, 非官方github)

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)

撰写回答